Implement Stream Container

This commit is contained in:
Chi Vinh Le
2017-04-24 19:27:54 +07:00
parent 4dc3b0c2f4
commit 11fb5421b5
7 changed files with 127 additions and 279 deletions
@@ -1,4 +1,10 @@
import * as actions from '../constants/embed';
import {viewAllComments} from './stream';
export const setActiveTab = (tab) => ({type: actions.SET_ACTIVE_TAB, tab});
export const setActiveTab = (tab) => (dispatch, getState) => {
dispatch({type: actions.SET_ACTIVE_TAB, tab});
if (getState().stream.commentId) {
dispatch(viewAllComments());
}
};
@@ -5,7 +5,7 @@ const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Button} from 'coral-ui';
import Stream from './Stream';
import Stream from '../containers/Stream';
import Count from 'coral-plugin-comment-count/CommentCount';
import UserBox from 'coral-sign-in/components/UserBox';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
@@ -17,75 +17,50 @@ export default class Embed extends React.Component {
switch(tab) {
case 0:
this.props.setActiveTab('stream');
this.props.data.refetch();
break;
case 1:
this.props.setActiveTab('profile');
// TODO: move data fetching to profile container.
this.props.data.refetch();
break;
case 2:
this.props.setActiveTab('config');
// TODO: move data fetching to config container.
this.props.data.refetch();
break;
}
}
render () {
const {activeTab} = this.props;
const {asset, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog} = this.props.auth;
const {activeTab, logout, viewAllComments, commentId} = this.props;
const {asset: {totalCommentCount}} = this.props.data;
const {loggedIn, isAdmin, user} = this.props.auth;
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
const userBox = <UserBox user={user} logout={this.props.logout} changeTab={this.changeTab}/>;
const userBox = <UserBox user={user} logout={logout} changeTab={this.changeTab}/>;
return (
<div style={expandForLogin}>
<div>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab><Count count={totalCommentCount}/></Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
comment &&
commentId &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={this.props.viewAllComments}
onClick={viewAllComments}
>
{lang.t('showAllComments')}
</Button>
}
<TabContent show={activeTab === 'stream'}>
{ loggedIn ? userBox : null }
<Stream
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
ignoreUser={this.props.ignoreUser}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments}
ignoredUsers={this.props.data.myIgnoredUsers ? this.props.data.myIgnoredUsers.map(u => u.id) : []}
auth={this.props.auth}
comment={this.props.data.comment}
commentCountCache={this.props.commentCountCache}
refetch={this.props.data.refetch}
editName={this.props.editName}
setCommentCountCache={this.props.setCommentCountCache}
/>
<Stream data={this.props.data} />
</TabContent>
<TabContent show={activeTab === 'profile'}>
<ProfileContainer />
@@ -107,13 +82,4 @@ Embed.propTypes = {
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
};
@@ -24,8 +24,7 @@ class Stream extends React.Component {
render () {
const {
comments,
asset,
data: {asset, asset: {comments}, comment, myIgnoredUsers},
postItem,
addNotification,
postFlag,
@@ -38,10 +37,7 @@ class Stream extends React.Component {
removeCommentTag,
pluginProps,
ignoreUser,
ignoredUsers,
auth: {loggedIn, isAdmin, user},
comment,
refetch,
commentCountCache,
editName,
} = this.props;
@@ -62,7 +58,7 @@ class Stream extends React.Component {
const firstCommentDate = asset.comments[0]
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id);
const commentIsIgnored = (comment) => myIgnoredUsers && myIgnoredUsers.includes(comment.user.id);
return (
<div id='stream'>
{
@@ -94,7 +90,6 @@ class Stream extends React.Component {
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={this.props.auth.user}
authorId={user.id}
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
: null
@@ -111,7 +106,6 @@ class Stream extends React.Component {
{
highlightedComment
? <Comment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
@@ -189,8 +183,6 @@ class Stream extends React.Component {
Stream.propTypes = {
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
asset: PropTypes.object.isRequired,
comments: PropTypes.array.isRequired,
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
@@ -200,9 +192,6 @@ Stream.propTypes = {
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
// list of user ids that should be rendered as ignored
ignoredUsers: React.PropTypes.arrayOf(React.PropTypes.string)
};
export default Stream;
+17 -200
View File
@@ -3,22 +3,15 @@ import {compose, gql, graphql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import isEqual from 'lodash/isEqual';
import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
import {Spinner} from 'coral-ui';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
import {authActions, assetActions, pym} from 'coral-framework';
import Embed from '../components/Embed';
import {setCommentCountCache, setActiveReplyBox, viewAllComments} from '../actions/stream';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {setActiveTab} from '../actions/embed';
import * as Stream from './Stream';
const {logout, showSignInDialog, requestConfirmEmail, checkLogin} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
@@ -28,32 +21,25 @@ class EmbedContainer extends React.Component {
this.props.checkLogin();
}
componentWillUnmount() {
clearInterval(this.countPoll);
}
componentWillReceiveProps(nextProps) {
if(this.props.data.me && !nextProps.data.me) {
// Refetch because on logout `excludeIgnored` becomes `false`.
this.props.data.refetch();
}
const {fetchAssetSuccess} = this.props;
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.data.asset);
const {getCounts, setCommentCountCache, commentCountCache} = this.props;
const {setCommentCountCache, commentCountCache} = this.props;
const {asset} = nextProps.data;
if (commentCountCache === -1) {
setCommentCountCache(asset.commentCount);
}
this.countPoll = setInterval(() => {
const {asset} = this.props.data;
getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
});
}, NEW_COMMENT_COUNT_POLL_INTERVAL);
}
}
@@ -73,172 +59,20 @@ class EmbedContainer extends React.Component {
}
}
const fragments = {
commentView: gql`
fragment commentView on Comment {
id
body
created_at
const EMBED_QUERY = gql`
query EmbedQuery($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
totalCommentCount(excludeIgnored: $excludeIgnored)
}
me {
status
tags {
name
}
user {
id
name: username
}
action_summaries {
...actionSummaryView
}
}
`,
actionSummaryView: gql`
fragment actionSummaryView on ActionSummary {
__typename
count
current_user {
id
created_at
}
}
`,
};
const LOAD_COMMENT_COUNTS_QUERY = gql`
query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) {
asset(id: $asset_id) {
id
commentCount
comments(sort: $sort, limit: $limit) {
id
replyCount
}
}
}
`;
const LOAD_MORE_QUERY = gql`
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
...commentView
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3) {
...commentView
}
}
}
${fragments.commentView}
${fragments.actionSummaryView}
`;
const STREAM_QUERY = gql`
query StreamQuery($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
__typename
...Stream_root
}
${Stream.fragments.root}
`;
// get the counts of the top-level comments
const getCounts = (data) => ({asset_id, limit, sort}) => {
return data.fetchMore({
query: LOAD_COMMENT_COUNTS_QUERY,
variables: {
asset_id,
limit,
sort,
excludeIgnored: data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{asset}}) => {
return {
...oldData,
asset: {
...oldData.asset,
commentCount: asset.commentCount
}
};
}
});
};
// handle paginated requests for more Comments pertaining to the Asset
const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => {
return data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit, // how many comments are we returning
cursor, // the date of the first/last comment depending on the sort order
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
asset_id, // the id of the asset we're currently on
sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
excludeIgnored: data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => {
let updatedAsset;
if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
let comment = {};
if (oldData.comment && oldData.comment.parent) {
// put comments (replies) onto the oldData.comment.parent object
// the initial comment permalinked was a reply
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id');
comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')};
} else if (oldData.comment) {
// put the comments (replies) directly onto oldData.comment
// the initial comment permalinked was a top-level comment
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id');
comment.replies = sortBy(uniqReplies, 'created_at');
}
updatedAsset = {
...oldData,
comment: {
...oldData.comment,
...comment
}
};
} else if (parent_id) { // If loading more replies
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map(comment => {
// since the dipslayed replies and the returned replies can overlap,
// pull out the unique ones.
const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id');
// since we just gave the returned replies precedence, they're now out of order.
// resort according to date.
return comment.id === parent_id
? {...comment, replies: sortBy(uniqueReplies, 'created_at')}
: 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 withQuery = graphql(STREAM_QUERY, {
export const withQuery = graphql(EMBED_QUERY, {
options: ({auth, commentId, assetId, assetUrl}) => ({
variables: {
assetId,
@@ -250,15 +84,12 @@ export const withQuery = graphql(STREAM_QUERY, {
}),
props: ({data}) => ({
data,
loadMore: loadMore(data),
getCounts: getCounts(data),
})
});
const mapStateToProps = state => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
@@ -267,30 +98,16 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch =>
bindActionCreators({
showSignInDialog,
requestConfirmEmail,
fetchAssetSuccess,
addNotification,
clearNotification,
checkLogin,
editName,
setCommentCountCache,
viewAllComments,
logout,
setActiveReplyBox,
setActiveTab,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postLike,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
withQuery,
)(EmbedContainer);
@@ -1,9 +1,21 @@
import React from 'react';
import {gql} from 'react-apollo';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
import Stream from '../components/Stream';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
export default class StreamContainer extends React.Component {
const {showSignInDialog} = authActions;
const {addNotification} = notificationActions;
class StreamContainer extends React.Component {
getCounts = ({asset_id, limit, sort}) => {
return this.props.data.fetchMore({
query: LOAD_COMMENT_COUNTS_QUERY,
@@ -11,7 +23,7 @@ export default class StreamContainer extends React.Component {
asset_id,
limit,
sort,
excludeIgnored: data.variables.excludeIgnored,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{asset}}) => {
return {
@@ -35,7 +47,7 @@ export default class StreamContainer extends React.Component {
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
asset_id, // the id of the asset we're currently on
sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
excludeIgnored: data.variables.excludeIgnored,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => {
let updatedAsset;
@@ -103,9 +115,10 @@ export default class StreamContainer extends React.Component {
};
componentDidMount() {
this.props.data.refetch();
this.countPoll = setInterval(() => {
const {asset} = this.props.data;
this.props.getCounts({
this.getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
@@ -118,7 +131,7 @@ export default class StreamContainer extends React.Component {
}
render() {
return <Stream {...this.props} loadMore={this.loadMore} getCounts={this.getCounts}/>;
return <Stream {...this.props} loadMore={this.loadMore}/>;
}
}
@@ -152,6 +165,33 @@ const actionSummaryViewFragment = gql`
}
`;
const LOAD_COMMENT_COUNTS_QUERY = gql`
query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) {
asset(id: $asset_id) {
id
commentCount
comments(sort: $sort, limit: $limit) {
id
replyCount
}
}
}
`;
const LOAD_MORE_QUERY = gql`
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
...commentView
replyCount(excludeIgnored: $excludeIgnored)
replies(limit: 3) {
...commentView
}
}
}
${commentViewFragment}
${actionSummaryViewFragment}
`;
StreamContainer.fragments = {
root: gql`
fragment Stream_root on RootQuery {
@@ -205,8 +245,44 @@ StreamContainer.fragments = {
id,
username,
}
me {
status
}
}
${commentViewFragment}
${actionSummaryViewFragment}
`,
};
const mapStateToProps = state => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
showSignInDialog,
addNotification,
setActiveReplyBox,
editName,
setCommentCountCache,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postLike,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
)(StreamContainer);
@@ -9,8 +9,6 @@ import REMOVE_COMMENT_TAG from './removeCommentTag.graphql';
import IGNORE_USER from './ignoreUser.graphql';
import STOP_IGNORING_USER from './stopIgnoringUser.graphql';
import MY_IGNORED_USERS from '../queries/myIgnoredUsers.graphql';
import commentView from '../fragments/commentView.graphql';
export const postComment = graphql(POST_COMMENT, {
@@ -43,7 +41,7 @@ export const postComment = graphql(POST_COMMENT, {
}
},
updateQueries: {
AssetQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
EmbedQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return oldData;
@@ -161,9 +159,9 @@ export const ignoreUser = graphql(IGNORE_USER, {
variables: {
id,
},
refetchQueries: [{
query: MY_IGNORED_USERS,
}]
refetchQueries: [
'EmbedQuery', 'myIgnoredUsers',
]
});
}}),
});
@@ -178,10 +176,7 @@ export const stopIgnoringUser = graphql(STOP_IGNORING_USER, {
id,
},
refetchQueries: [
{
query: MY_IGNORED_USERS,
},
'StreamQuery',
'EmbedQuery', 'myIgnoredUsers',
]
});
}
@@ -196,7 +196,6 @@ CommentBox.propTypes = {
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
currentUser: PropTypes.object
};
const mapStateToProps = ({commentBox}) => ({commentBox});