Files
talk/client/coral-embed-stream/src/containers/Embed.js
T

292 lines
8.3 KiB
JavaScript

import React from 'react';
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 Embed from '../components/Embed';
import {setCommentCountCache, setActiveReplyBox, viewAllComments} from '../actions/stream';
import * as Stream from './Stream';
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
componentDidMount() {
pym.sendMessage('childReady');
}
componentWillUnmount() {
clearInterval(this.countPoll);
}
componentWillReceiveProps(nextProps) {
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 {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);
}
}
componentDidUpdate(prevProps) {
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
}
}
render() {
if (!this.props.data.asset) {
return <Spinner />;
}
return <Embed {...this.props} />;
}
}
const fragments = {
commentView: gql`
fragment commentView on Comment {
id
body
created_at
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.fragment}
`;
// 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, {
options: ({auth, commentId, assetId, assetUrl}) => ({
variables: {
assetId,
assetUrl,
commentId,
hasComment: commentId !== '',
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
},
}),
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,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
showSignInDialog,
requestConfirmEmail,
fetchAssetSuccess,
addNotification,
clearNotification,
editName,
setCommentCountCache,
viewAllComments,
logout,
setActiveReplyBox,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postLike,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
withQuery,
)(EmbedContainer);