Merge branch 'master' into comment-design-pass

This commit is contained in:
David Jay
2017-02-23 16:28:20 -05:00
committed by GitHub
16 changed files with 236 additions and 41 deletions
+1 -1
View File
@@ -177,7 +177,7 @@ class Comment extends React.Component {
comment.replies &&
<div className='coral-load-more-replies'>
<LoadMore
id={asset.id}
assetId={asset.id}
comments={comment.replies}
parentId={comment.id}
moreComments={comment.replyCount > comment.replies.length}
+21 -1
View File
@@ -12,6 +12,7 @@ const {fetchAssetSuccess} = assetActions;
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, deleteAction} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache} from 'coral-framework/actions/asset';
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
@@ -28,6 +29,7 @@ import SettingsContainer from 'coral-settings/containers/SettingsContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
class Embed extends Component {
@@ -82,7 +84,7 @@ class Embed extends Component {
render () {
const {activeTab} = this.state;
const {closedAt} = this.props.asset;
const {closedAt, countCache = {}} = this.props.asset;
const {loading, asset, refetch} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
@@ -98,6 +100,11 @@ class Embed extends Component {
return <Spinner />;
}
// Find the created_at date of the first comment. If no comments exist, set the date to a week ago.
const firstCommentDate = asset.comments[0]
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
return (
<div style={expandForLogin}>
<div className="commentStream">
@@ -132,6 +139,8 @@ class Embed extends Component {
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
updateCountCache={this.props.updateCountCache}
countCache={countCache[asset.id]}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
@@ -147,6 +156,14 @@ class Embed extends Component {
}
{!loggedIn && <SignInContainer requireEmailConfirmation={asset.settings.requireEmailConfirmation} offset={signInOffset}/>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<Stream
refetch={refetch}
addNotification={this.props.addNotification}
@@ -155,6 +172,8 @@ class Embed extends Component {
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
getCounts={this.props.getCounts}
updateCountCache={this.props.updateCountCache}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
@@ -216,6 +235,7 @@ const mapDispatchToProps = dispatch => ({
clearNotification: () => dispatch(clearNotification()),
editName: (username) => dispatch(editName(username)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
});
+1 -1
View File
@@ -12,7 +12,7 @@ const loadMoreComments = (assetId, comments, loadMore, parentId) => {
}
const cursor = parentId
? comments[1].created_at
? comments[0].created_at
: comments[comments.length - 1].created_at;
loadMore({
+40
View File
@@ -0,0 +1,40 @@
import React, {PropTypes} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, updateCountCache}) => (e) => {
e.preventDefault();
updateCountCache(assetId, commentCount);
loadMore({
limit: 500,
cursor: firstCommentDate,
assetId,
sort: 'CHRONOLOGICAL'
}, true);
};
const NewCount = (props) => {
const newComments = props.commentCount - props.countCache;
return <div className='coral-new-comments'>
{
props.countCache && newComments > 0 &&
<button onClick={onLoadMoreClick(props)} className='coral-load-more'>
{newComments === 1
? lang.t('newCount', newComments, lang.t('comment'))
: lang.t('newCount', newComments, lang.t('comments'))}
</button>
}
</div>;
};
NewCount.propTypes = {
commentCount: PropTypes.number.isRequired,
countCache: PropTypes.number,
loadMore: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired,
firstCommentDate: PropTypes.string.isRequired
};
export default NewCount;
+22 -1
View File
@@ -1,5 +1,6 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
class Stream extends React.Component {
@@ -17,10 +18,30 @@ class Stream extends React.Component {
constructor(props) {
super(props);
this.state = {activeReplyBox: ''};
this.state = {activeReplyBox: '', countPoll: null};
this.setActiveReplyBox = this.setActiveReplyBox.bind(this);
}
componentDidMount() {
const {asset, getCounts, updateCountCache} = this.props;
updateCountCache(asset.id, asset.commentCount);
// Note: Apollo's built-in polling doesn't work with fetchMore queries, so a
// setInterval is being used instead.
this.setState({
countPoll: setInterval(() => getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
}), NEW_COMMENT_COUNT_POLL_INTERVAL),
});
}
componentWillUnmount() {
clearInterval(this.state.countPoll);
}
setActiveReplyBox (reactKey) {
if (!this.props.currentUser) {
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
+10 -2
View File
@@ -356,18 +356,26 @@ button.coral-load-more {
text-align: center;
color: #FFF;
background-color: #2376D8;
cursor: pointer;
}
button.coral-load-more:hover {
background-color: #4399FF;
}
.coral-load-more-replies {
.coral-load-more-replies, .coral-new-comments {
width: 100%;
display: flex;
justify-content: center;
cursor: pointer;
}
.coral-load-more-replies button.coral-load-more {
.coral-new-comments {
position: relative;
top: 1.8em;
z-index: 100;
}
.coral-load-more-replies button.coral-load-more, .coral-new-comments button.coral-load-more{
width: initial;
}
+1
View File
@@ -38,6 +38,7 @@ export const updateOpenStream = closedBody => (dispatch, getState) => {
const openStream = () => ({type: actions.OPEN_COMMENTS});
const closeStream = () => ({type: actions.CLOSE_COMMENTS});
export const updateCountCache = (id, count) => ({type: actions.UPDATE_COUNT_CACHE, id, count});
export const updateOpenStatus = status => dispatch => {
if (status === 'open') {
@@ -8,3 +8,4 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE';
@@ -1 +1,2 @@
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
+69 -34
View File
@@ -1,6 +1,7 @@
import {graphql} from 'react-apollo';
import STREAM_QUERY from './streamQuery.graphql';
import LOAD_MORE from './loadMore.graphql';
import GET_COUNTS from './getCounts.graphql';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
function getQueryVariable(variable) {
@@ -17,6 +18,72 @@ function getQueryVariable(variable) {
return 'http://localhost/default/stream';
}
export const getCounts = (data) => ({asset_id, limit, sort}) => {
return data.fetchMore({
query: GET_COUNTS,
variables: {
asset_id,
limit,
sort
},
updateQuery: (oldData, {fetchMoreResult:{data}}) => {
return {
...oldData,
asset: {
...oldData.asset,
commentCount: data.asset.commentCount
}
};
}
});
};
export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, newComments) => {
return data.fetchMore({
query: LOAD_MORE,
variables: {
limit,
cursor,
parent_id,
asset_id,
sort
},
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => {
let updatedAsset;
if (parent_id) {
// If loading more replies
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map((comment) =>
comment.id === parent_id
? {...comment, replies: [...comment.replies, ...new_top_level_comments]}
: comment)
}
};
} else {
// If loading more top-level comments
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments]
: [...oldData.asset.comments, ...new_top_level_comments]
}
};
}
return updatedAsset;
}
});
};
export const queryStream = graphql(STREAM_QUERY, {
options: () => ({
variables: {
@@ -25,40 +92,8 @@ export const queryStream = graphql(STREAM_QUERY, {
}),
props: ({data}) => ({
data,
loadMore: ({limit, cursor, parent_id, asset_id, sort}) => {
return data.fetchMore({
query: LOAD_MORE,
variables: {
limit,
cursor,
parent_id,
asset_id,
sort
},
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) =>
// If loading more replies
parent_id ? {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map((comment) =>
comment.id === parent_id
? {...comment, replies: [...comment.replies, ...new_top_level_comments]}
: comment)
}
}
// If loading more top-level comments
: {
...oldData,
asset: {
...oldData.asset,
comments: [...oldData.asset.comments, ...new_top_level_comments]
}
}
});
}
loadMore: loadMore(data),
getCounts: getCounts(data),
})
});
+3
View File
@@ -19,6 +19,9 @@ export default function asset (state = initialState, action) {
case actions.UPDATE_ASSET_SETTINGS_SUCCESS:
return state
.setIn(['settings'], action.settings);
case actions.UPDATE_COUNT_CACHE:
return state
.setIn(['countCache', action.id], action.count);
default:
return state;
}
+6
View File
@@ -11,6 +11,9 @@
"button": "Submit",
"error": "Usernames can contain letters, numbers and _ only"
},
"newCount": "View {0} more {1}",
"comment": "comment",
"comments": "comments",
"error": {
"emailNotVerified": "Email address {0} not verified.",
"email": "Not a valid E-Mail",
@@ -39,6 +42,9 @@
"bannedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"editNameMsg": "",
"loadMore": "Ver más",
"newCount": "Ver {0} {1} más",
"comment": "commentario",
"comments": "commentarios",
"error": {
"emailNotVerified": "Dirección de correo electrónico {0} no verificada.",
"email": "No es un email válido",
@@ -29,6 +29,8 @@ class CommentBox extends Component {
commentPostedHandler,
postItem,
assetId,
updateCountCache,
countCache,
parentId,
addNotification,
authorId
@@ -44,14 +46,17 @@ class CommentBox extends Component {
if (this.props.charCount && this.state.body.length > this.props.charCount) {
return;
}
updateCountCache(assetId, countCache + 1);
postItem(comment, 'comments')
.then(({data}) => {
const postedComment = data.createComment.comment;
if (postedComment.status === 'REJECTED') {
addNotification('error', lang.t('comment-post-banned-word'));
updateCountCache(assetId, countCache);
} else if (postedComment.status === 'PREMOD') {
addNotification('success', lang.t('comment-post-notif-premod'));
updateCountCache(assetId, countCache);
} else {
addNotification('success', 'Your comment has been posted.');
}
+1 -1
View File
@@ -448,7 +448,7 @@ type RootQuery {
# Comments returned based on a query.
comments(query: CommentsQuery!): [Comment]
# Returne the count of comments satisfied by the query. Note that this edge is
# Return the count of comments satisfied by the query. Note that this edge is
# expensive as it is not batched. Requires the `ADMIN` role.
commentCount(query: CommentCountQuery!): Int
@@ -0,0 +1,19 @@
import {Map} from 'immutable';
import {expect} from 'chai';
import assetReducer from '../../../../client/coral-framework/reducers/asset';
import * as actions from '../../../../client/coral-framework/constants/asset';
describe ('coral-embed-stream assetReducer', () => {
describe('UPDATE_COUNT_CACHE', () => {
it('should update the count cache', () => {
const action = {
type: actions.UPDATE_COUNT_CACHE,
id: '123',
count: 456
};
const store = new Map({});
const result = assetReducer(store, action);
expect(result.getIn(['countCache', '123'])).to.equal(456);
});
});
});
@@ -0,0 +1,35 @@
import {Map} from 'immutable';
import {expect} from 'chai';
import notificationReducer from '../../../../client/coral-framework/reducers/notification';
import * as actions from '../../../../client/coral-framework/actions/notification';
describe ('notificationsReducer', () => {
describe('ADD_NOTIFICATION', () => {
it('should add a notification', () => {
const action = {
type: actions.ADD_NOTIFICATION,
text: 'Test notification',
notifType: 'test'
};
const store = new Map({});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal(action.text);
expect(result.get('type')).to.equal(action.notifType);
});
});
describe('CLEAR_NOTIFICATION', () => {
it('should clear a notification', () => {
const action = {
type: actions.CLEAR_NOTIFICATION
};
const store = new Map({
text: 'Test notification',
type: 'test'
});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal('');
expect(result.get('type')).to.equal('');
});
});
});