mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge branch 'master' into bulk-actions
This commit is contained in:
@@ -2,7 +2,6 @@ import {pym} from 'coral-framework';
|
||||
import * as actions from '../constants/stream';
|
||||
|
||||
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
|
||||
export const setCommentCountCache = (amount) => ({type: actions.SET_COMMENT_COUNT_CACHE, amount});
|
||||
|
||||
function removeParam(key, sourceURL) {
|
||||
let rtn = sourceURL.split('?')[0];
|
||||
|
||||
@@ -97,3 +97,13 @@
|
||||
.Wizard .textAlignRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@keyframes enter {
|
||||
0% {background-color: rgba(0, 0, 0, 0);}
|
||||
50% {background-color: rgba(255,255,0, 0.2);}
|
||||
100% {background-color: rgba(0, 0, 0, 0);}
|
||||
}
|
||||
|
||||
.enter {
|
||||
animation: enter 1000ms;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import Content from 'coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from 'coral-plugin-pubdate/PubDate';
|
||||
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
|
||||
import FlagComment from 'coral-plugin-flags/FlagComment';
|
||||
import {TransitionGroup} from 'react-transition-group';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {
|
||||
BestButton,
|
||||
IfUserCanModifyBest,
|
||||
@@ -19,13 +22,47 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import LoadMore from './LoadMore';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import {TopRightMenu} from './TopRightMenu';
|
||||
import classnames from 'classnames';
|
||||
import {EditableCommentContent} from './EditableCommentContent';
|
||||
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import styles from './Comment.css';
|
||||
|
||||
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF');
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second newest comment in
|
||||
// the current reply list. The cursors are used to dertermine which
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
function resetCursors(state, props) {
|
||||
const replies = props.comment.replies;
|
||||
if (replies && replies.nodes.length) {
|
||||
const idCursors = [replies.nodes[replies.nodes.length - 1].id];
|
||||
if (replies.nodes.length >= 2) {
|
||||
idCursors.push(replies.nodes[replies.nodes.length - 2].id);
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
return {idCursors: []};
|
||||
}
|
||||
|
||||
// invalidateCursor is called whenever a comment is removed which is referenced
|
||||
// by one of the 2 id cursors. It returns a new set of id cursors calculated
|
||||
// using the help of the backup cursor.
|
||||
function invalidateCursor(invalidated, state, props) {
|
||||
const alt = invalidated === 1 ? 0 : 1;
|
||||
const replies = props.comment.replies;
|
||||
const idCursors = [];
|
||||
if (state.idCursors[alt]) {
|
||||
idCursors.push(state.idCursors[alt]);
|
||||
const index = replies.nodes.findIndex((node) => node.id === idCursors[0]);
|
||||
const prevInLine = replies.nodes[index - 1];
|
||||
if (prevInLine) {
|
||||
idCursors.push(prevInLine.id);
|
||||
}
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
|
||||
// hold actions links (e.g. Reply) along the comment footer
|
||||
const ActionButton = ({children}) => {
|
||||
@@ -37,6 +74,7 @@ const ActionButton = ({children}) => {
|
||||
};
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -49,9 +87,47 @@ class Comment extends React.Component {
|
||||
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
|
||||
isEditing: false,
|
||||
replyBoxVisible: false,
|
||||
animateEnter: false,
|
||||
...resetCursors({}, props),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const {comment: {replies: prevReplies}} = this.props;
|
||||
const {comment: {replies: nextReplies}} = next;
|
||||
if (
|
||||
prevReplies && nextReplies &&
|
||||
nextReplies.nodes.length < prevReplies.nodes.length
|
||||
) {
|
||||
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextReplies.nodes, this.state.idCursors[0])) {
|
||||
this.setState(invalidateCursor(0, this.state, next));
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextReplies.nodes, this.state.idCursors[1])) {
|
||||
this.setState(invalidateCursor(1, this.state, next));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillEnter(callback) {
|
||||
callback();
|
||||
const userId = this.props.currentUser ? this.props.currentUser.id : null;
|
||||
if (this.props.comment.id.indexOf('pending') >= 0) {
|
||||
return;
|
||||
}
|
||||
if (userId && this.props.comment.user.id === userId) {
|
||||
|
||||
// This comment was just added by currentUser.
|
||||
if (Date.now() - Number(new Date(this.props.comment.created_at)) < 30 * 1000) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.setState({animateEnter: true});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
reactKey: PropTypes.string.isRequired,
|
||||
|
||||
@@ -67,6 +143,7 @@ class Comment extends React.Component {
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
liveUpdates: PropTypes.bool.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
@@ -127,6 +204,50 @@ class Comment extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
hasIgnoredReplies() {
|
||||
return this.props.comment.replies &&
|
||||
this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply));
|
||||
}
|
||||
|
||||
loadNewReplies = () => {
|
||||
const {replies, replyCount, id} = this.props.comment;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.props.loadMore(id).then(() => {
|
||||
this.setState(resetCursors(this.state, this.props));
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
// getVisibileReplies returns a list containing comments
|
||||
// which were authored by current user or comes before the `idCursor`.
|
||||
getVisibileReplies() {
|
||||
const {comment: {replies}, currentUser, liveUpdates} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
const userId = currentUser ? currentUser.id : null;
|
||||
|
||||
if (!replies) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (liveUpdates) {
|
||||
return replies.nodes;
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
replies.nodes.forEach((comment) => {
|
||||
if (idCursor && !pastCursor || comment.user.id === userId) {
|
||||
view.push(comment);
|
||||
}
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
@@ -162,19 +283,22 @@ class Comment extends React.Component {
|
||||
highlighted,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
loadMore,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
deleteAction,
|
||||
addCommentTag,
|
||||
removeCommentTag,
|
||||
ignoreUser,
|
||||
liveUpdates,
|
||||
disableReply,
|
||||
commentIsIgnored,
|
||||
maxCharCount,
|
||||
charCountEnable
|
||||
} = this.props;
|
||||
|
||||
const view = this.getVisibileReplies();
|
||||
const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length);
|
||||
const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount;
|
||||
const flagSummary = getActionSummary('FlagActionSummary', comment);
|
||||
const dontAgreeSummary = getActionSummary(
|
||||
'DontAgreeActionSummary',
|
||||
@@ -226,7 +350,7 @@ class Comment extends React.Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={commentClass}
|
||||
className={cn(commentClass, 'talk-stream-comment-wrapper', {[styles.enter]: this.state.animateEnter})}
|
||||
id={`c_${comment.id}`}
|
||||
style={{marginLeft: depth * 30}}
|
||||
>
|
||||
@@ -234,15 +358,15 @@ class Comment extends React.Component {
|
||||
<div
|
||||
className={highlighted === comment.id ? 'highlighted-comment' : ''}
|
||||
>
|
||||
<AuthorName author={comment.user} />
|
||||
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
{commentIsBest(comment)
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
: null }
|
||||
|
||||
<span className={styles.bylineSecondary}>
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
|
||||
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>(Edited)</span></span>
|
||||
@@ -263,17 +387,17 @@ class Comment extends React.Component {
|
||||
(comment.user.id === currentUser.id))
|
||||
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
? <span className={classnames(styles.topRight)}>
|
||||
? <span className={cn(styles.topRight)}>
|
||||
{
|
||||
commentIsStillEditable(comment) &&
|
||||
<a
|
||||
className={classnames(styles.link, {[styles.active]: this.state.isEditing})}
|
||||
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
|
||||
onClick={this.onClickEdit}>Edit</a>
|
||||
}
|
||||
</span>
|
||||
|
||||
/* TopRightMenu allows currentUser to ignore other users' comments */
|
||||
: <span className={classnames(styles.topRight, styles.topRightMenu)}>
|
||||
: <span className={cn(styles.topRight, styles.topRightMenu)}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
@@ -369,46 +493,49 @@ class Comment extends React.Component {
|
||||
assetId={asset.id}
|
||||
/>
|
||||
: null}
|
||||
{comment.replies &&
|
||||
comment.replies.nodes.map((reply) => {
|
||||
return commentIsIgnored(reply)
|
||||
? <IgnoredCommentTombstone key={reply.id} />
|
||||
: <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
addCommentTag={addCommentTag}
|
||||
removeCommentTag={removeCommentTag}
|
||||
ignoreUser={ignoreUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
/>;
|
||||
})}
|
||||
{comment.replies &&
|
||||
<div className="coral-load-more-replies">
|
||||
<LoadMore
|
||||
topLevel={false}
|
||||
replyCount={comment.replyCount}
|
||||
moreComments={comment.replyCount > comment.replies.nodes.length}
|
||||
loadMore={() => loadMore(comment.id)}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
<TransitionGroup>
|
||||
{view.map((reply) => {
|
||||
return commentIsIgnored(reply)
|
||||
? <IgnoredCommentTombstone key={reply.id} />
|
||||
: <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
addCommentTag={addCommentTag}
|
||||
removeCommentTag={removeCommentTag}
|
||||
ignoreUser={ignoreUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
liveUpdates={liveUpdates}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
/>;
|
||||
})}
|
||||
</TransitionGroup>
|
||||
<div className="coral-load-more-replies">
|
||||
<LoadMore
|
||||
topLevel={false}
|
||||
replyCount={replyCount}
|
||||
moreComments={hasMoreComments}
|
||||
loadMore={this.loadNewReplies}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ export default class Embed extends React.Component {
|
||||
<div>
|
||||
<div className="commentStream">
|
||||
<TabBar onChange={this.changeTab} activeTab={activeTab} className='talk-stream-tabbar'>
|
||||
<Tab className={'talk-stream-comment-count-tab'}><Count count={totalCommentCount}/></Tab>
|
||||
<Tab className={'talk-stream-profile-tab'}>{t('framework.my_profile')}</Tab>
|
||||
<Tab className={'talk-stream-configuration-tab'} restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
|
||||
<Tab className={'talk-stream-comment-count-tab'} id='stream'><Count count={totalCommentCount}/></Tab>
|
||||
<Tab className={'talk-stream-profile-tab'} id='profile'>{t('framework.my_profile')}</Tab>
|
||||
<Tab className={'talk-stream-configuration-tab'} id='config' restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
|
||||
</TabBar>
|
||||
{commentId &&
|
||||
<Button
|
||||
|
||||
@@ -3,18 +3,22 @@ import React from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Render in place of a Comment when the author of the comment is ignored
|
||||
const IgnoredCommentTombstone = () => (
|
||||
<div>
|
||||
<hr aria-hidden={true} />
|
||||
<p style={{
|
||||
backgroundColor: '#F0F0F0',
|
||||
textAlign: 'center',
|
||||
padding: '1em',
|
||||
color: '#3E4F71',
|
||||
}}>
|
||||
{t('framework.comment_is_ignored')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
class IgnoredCommentTombstone extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<hr aria-hidden={true} />
|
||||
<p style={{
|
||||
backgroundColor: '#F0F0F0',
|
||||
textAlign: 'center',
|
||||
padding: '1em',
|
||||
color: '#3E4F71',
|
||||
}}>
|
||||
{t('framework.comment_is_ignored')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IgnoredCommentTombstone;
|
||||
|
||||
@@ -9,6 +9,9 @@ class LoadMore extends React.Component {
|
||||
}
|
||||
|
||||
replyCountFormat = (count) => {
|
||||
if (!count) {
|
||||
return t('framework.view_all_replies_unknown_number');
|
||||
}
|
||||
if (count === 1) {
|
||||
return t('framework.view_reply');
|
||||
}
|
||||
|
||||
@@ -2,22 +2,14 @@ import React, {PropTypes} from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const onLoadMoreClick = ({loadMore, commentCount, setCommentCountCache}) => (e) => {
|
||||
e.preventDefault();
|
||||
setCommentCountCache(commentCount);
|
||||
loadMore();
|
||||
};
|
||||
|
||||
const NewCount = (props) => {
|
||||
const newComments = props.commentCount - props.commentCountCache;
|
||||
|
||||
const NewCount = ({count, loadMore}) => {
|
||||
return <div className='coral-new-comments coral-load-more'>
|
||||
{
|
||||
props.commentCountCache && newComments > 0 ?
|
||||
<button onClick={onLoadMoreClick(props)}>
|
||||
{newComments === 1
|
||||
? t('framework.new_count', newComments, t('framework.comment'))
|
||||
: t('framework.new_count', newComments, t('framework.comments'))}
|
||||
count ?
|
||||
<button onClick={loadMore}>
|
||||
{count === 1
|
||||
? t('framework.new_count', count, t('framework.comment'))
|
||||
: t('framework.new_count', count, t('framework.comments'))}
|
||||
</button>
|
||||
: null
|
||||
}
|
||||
@@ -25,8 +17,7 @@ const NewCount = (props) => {
|
||||
};
|
||||
|
||||
NewCount.propTypes = {
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
commentCountCache: PropTypes.number,
|
||||
count: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import LoadMore from './LoadMore';
|
||||
import NewCount from './NewCount';
|
||||
|
||||
import Comment from '../containers/Comment';
|
||||
import Comment from '../components/Comment';
|
||||
import SuspendedAccount from './SuspendedAccount';
|
||||
import RestrictedMessageBox
|
||||
from 'coral-framework/components/RestrictedMessageBox';
|
||||
@@ -13,9 +12,82 @@ import {ModerationLink} from 'coral-plugin-moderation';
|
||||
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
||||
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import NewCount from './NewCount';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import {TransitionGroup} from 'react-transition-group';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second comment of
|
||||
// the current comment list. The cursors are used to dertermine which
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
function resetCursors(state, props) {
|
||||
const comments = props.root.asset.comments;
|
||||
if (comments && comments.nodes.length) {
|
||||
const idCursors = [comments.nodes[0].id];
|
||||
if (comments.nodes[1]) {
|
||||
idCursors.push(comments.nodes[1].id);
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
return {idCursors: []};
|
||||
}
|
||||
|
||||
// invalidateCursor is called whenever a comment is removed which is referenced
|
||||
// by one of the 2 id cursors. It returns a new set of id cursors calculated
|
||||
// using the help of the backup cursor.
|
||||
function invalidateCursor(invalidated, state, props) {
|
||||
const alt = invalidated === 1 ? 0 : 1;
|
||||
const comments = props.root.asset.comments;
|
||||
const idCursors = [];
|
||||
if (state.idCursors[alt]) {
|
||||
idCursors.push(state.idCursors[alt]);
|
||||
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
|
||||
const nextInLine = comments.nodes[index + 1];
|
||||
if (nextInLine) {
|
||||
idCursors.push(nextInLine.id);
|
||||
}
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
|
||||
class Stream extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = resetCursors(this.state, props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const {root: {asset: {comments: prevComments}}} = this.props;
|
||||
const {root: {asset: {comments: nextComments}}} = next;
|
||||
|
||||
if (!prevComments && nextComments) {
|
||||
this.setState(resetCursors);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
nextComments.nodes.length < prevComments.nodes.length
|
||||
) {
|
||||
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
|
||||
this.setState(invalidateCursor(0, this.state, next));
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
|
||||
this.setState(invalidateCursor(1, this.state, next));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewNewComments = () => {
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
setActiveReplyBox = (reactKey) => {
|
||||
if (!this.props.auth.user) {
|
||||
this.props.showSignInDialog();
|
||||
@@ -24,6 +96,30 @@ class Stream extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which were authored by current user or comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
const {root: {asset: {comments}}, auth: {user}} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
const userId = user ? user.id : null;
|
||||
|
||||
if (!comments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
comments.nodes.forEach((comment) => {
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
if (pastCursor || comment.user.id === userId) {
|
||||
view.push(comment);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
root: {asset, asset: {comments}, comment, me},
|
||||
@@ -38,9 +134,9 @@ class Stream extends React.Component {
|
||||
pluginProps,
|
||||
ignoreUser,
|
||||
auth: {loggedIn, user},
|
||||
commentCountCache,
|
||||
editName
|
||||
} = this.props;
|
||||
const view = this.getVisibleComments();
|
||||
const open = asset.closedAt === null;
|
||||
|
||||
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
|
||||
@@ -97,8 +193,6 @@ class Stream extends React.Component {
|
||||
postComment={this.props.postComment}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
setCommentCountCache={this.props.setCommentCountCache}
|
||||
commentCountCache={commentCountCache}
|
||||
assetId={asset.id}
|
||||
premod={asset.settings.moderation}
|
||||
isReply={false}
|
||||
@@ -139,16 +233,15 @@ class Stream extends React.Component {
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={this.props.editComment}
|
||||
liveUpdates={true}
|
||||
/>
|
||||
: <div>
|
||||
<NewCount
|
||||
commentCount={asset.commentCount}
|
||||
commentCountCache={commentCountCache}
|
||||
setCommentCountCache={this.props.setCommentCountCache}
|
||||
loadMore={this.props.loadNewComments}
|
||||
count={comments.nodes.length - view.length}
|
||||
loadMore={this.viewNewComments}
|
||||
/>
|
||||
<div className="embed__stream">
|
||||
{comments && comments.nodes.map((comment) => {
|
||||
<TransitionGroup component='div' className="embed__stream">
|
||||
{view.map((comment) => {
|
||||
return commentIsIgnored(comment)
|
||||
? <IgnoredCommentTombstone key={comment.id} />
|
||||
: <Comment
|
||||
@@ -178,9 +271,10 @@ class Stream extends React.Component {
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={this.props.editComment}
|
||||
liveUpdates={false}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
moreComments={asset.comments.hasNextPage}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
|
||||
export const SET_COMMENT_COUNT_CACHE = 'SET_COMMENT_COUNT_CACHE';
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
|
||||
export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
|
||||
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
|
||||
@@ -14,7 +14,7 @@ import Embed from '../components/Embed';
|
||||
import Stream from './Stream';
|
||||
|
||||
import {setActiveTab} from '../actions/embed';
|
||||
import {setCommentCountCache, viewAllComments} from '../actions/stream';
|
||||
import {viewAllComments} from '../actions/stream';
|
||||
|
||||
const {logout, checkLogin} = authActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
@@ -33,18 +33,11 @@ class EmbedContainer extends React.Component {
|
||||
|
||||
// TODO: remove asset data from redux store.
|
||||
fetchAssetSuccess(nextProps.root.asset);
|
||||
|
||||
const {setCommentCountCache, commentCountCache} = this.props;
|
||||
const {asset} = nextProps.root;
|
||||
|
||||
if (commentCountCache === -1) {
|
||||
setCommentCountCache(asset.commentCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!isEqual(prevProps.root.comment, this.props.root.comment)) {
|
||||
if (!prevProps.root.comment && this.props.root.comment) {
|
||||
|
||||
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
|
||||
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
|
||||
@@ -87,7 +80,6 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
commentCountCache: state.stream.commentCountCache,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
assetUrl: state.stream.assetUrl,
|
||||
@@ -103,7 +95,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||
setActiveTab,
|
||||
viewAllComments,
|
||||
fetchAssetSuccess,
|
||||
setCommentCountCache
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
@@ -2,34 +2,94 @@ import React from 'react';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {NEW_COMMENT_COUNT_POLL_INTERVAL, ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
|
||||
import {
|
||||
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
|
||||
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
import {notificationActions, authActions} from 'coral-framework';
|
||||
import {editName} from 'coral-framework/actions/user';
|
||||
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
|
||||
import {setActiveReplyBox} from '../actions/stream';
|
||||
import Stream from '../components/Stream';
|
||||
import Comment from './Comment';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {
|
||||
findCommentInEmbedQuery,
|
||||
insertCommentIntoEmbedQuery,
|
||||
removeCommentFromEmbedQuery,
|
||||
insertFetchedCommentsIntoEmbedQuery,
|
||||
} from '../graphql/utils';
|
||||
|
||||
const {showSignInDialog} = authActions;
|
||||
const {addNotification} = notificationActions;
|
||||
|
||||
class StreamContainer extends React.Component {
|
||||
getCounts = (variables) => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_COMMENT_COUNTS_QUERY,
|
||||
variables,
|
||||
subscriptions = [];
|
||||
|
||||
// Apollo requires this, even though we don't use it...
|
||||
updateQuery: (data) => data,
|
||||
subscribeToUpdates() {
|
||||
const sub1 = this.props.data.subscribeToMore({
|
||||
document: COMMENTS_EDITED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
|
||||
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (this.props.auth.user && commentEdited.user.id === this.props.auth.user.id) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Exit when comment is not in the query.
|
||||
if (!findCommentInEmbedQuery(prev, commentEdited.id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (['PREMOD', 'REJECTED'].includes(commentEdited.status)) {
|
||||
return removeCommentFromEmbedQuery(prev, commentEdited.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sub2 = this.props.data.subscribeToMore({
|
||||
document: COMMENTS_ADDED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
|
||||
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (this.props.auth.user && commentAdded.user.id === this.props.auth.user.id) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Exit if author is ignored.
|
||||
if (
|
||||
this.props.root.me &&
|
||||
this.props.root.me.ignoredUsers.some(({id}) => id === commentAdded.user.id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Exit when comment is already in the query.
|
||||
if (findCommentInEmbedQuery(prev, commentAdded.id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return insertCommentIntoEmbedQuery(prev, commentAdded);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscriptions.push(sub1, sub2);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
loadNewReplies = (parent_id) => {
|
||||
const comment = this.props.root.comment
|
||||
@@ -47,86 +107,11 @@ class StreamContainer extends React.Component {
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
if (!comments.nodes.length) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const updateNode = (node) =>
|
||||
update(node, {
|
||||
replies: {
|
||||
endCursor: {$set: comments.endCursor},
|
||||
nodes: {$apply: (nodes) => nodes
|
||||
.concat(comments.nodes.filter(
|
||||
(comment) => !nodes.some((node) => node.id === comment.id)
|
||||
))
|
||||
.sort(ascending)
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// highlighted comment.
|
||||
if (prev.comment) {
|
||||
if (prev.comment.parent) {
|
||||
return update(prev, {
|
||||
comment: {
|
||||
parent: {$apply: (comment) => updateNode(comment)},
|
||||
}
|
||||
});
|
||||
}
|
||||
return update(prev, {
|
||||
comment: {$apply: (comment) => updateNode(comment)},
|
||||
});
|
||||
}
|
||||
|
||||
return update(prev, {
|
||||
asset: {
|
||||
comments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => nodes.map(
|
||||
(node) => node.id !== parent_id
|
||||
? node
|
||||
: updateNode(node)
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return insertFetchedCommentsIntoEmbedQuery(prev, comments, parent_id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
loadNewComments = () => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
cursor: this.props.root.asset.comments.startCursor,
|
||||
parent_id: null,
|
||||
asset_id: this.props.root.asset.id,
|
||||
sort: 'CHRONOLOGICAL',
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
if (!comments.nodes.length) {
|
||||
return prev;
|
||||
}
|
||||
return update(prev, {
|
||||
asset: {
|
||||
comments: {
|
||||
startCursor: {$set: comments.endCursor},
|
||||
nodes: {$apply: (nodes) => comments.nodes.filter(
|
||||
(comment) => !nodes.some((node) => node.id === comment.id)
|
||||
)
|
||||
.concat(nodes)
|
||||
.sort(descending)
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
@@ -139,78 +124,77 @@ class StreamContainer extends React.Component {
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
if (!comments.nodes.length) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return update(prev, {
|
||||
asset: {
|
||||
comments: {
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
nodes: {$push: comments.nodes},
|
||||
},
|
||||
},
|
||||
});
|
||||
return insertFetchedCommentsIntoEmbedQuery(prev, comments);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.previousTab) {
|
||||
this.props.data.refetch()
|
||||
.then(({data: {asset: {commentCount}}}) => {
|
||||
return this.props.setCommentCountCache(commentCount);
|
||||
});
|
||||
this.props.data.refetch();
|
||||
}
|
||||
this.countPoll = setInterval(() => {
|
||||
this.getCounts(this.props.data.variables);
|
||||
}, NEW_COMMENT_COUNT_POLL_INTERVAL);
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
clearInterval(this.countPoll);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.refetching) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <Stream
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
loadMoreComments={this.loadMoreComments}
|
||||
loadNewComments={this.loadNewComments}
|
||||
loadNewReplies={this.loadNewReplies}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const ascending = (a, b) => {
|
||||
const dateA = new Date(a.created_at);
|
||||
const dateB = new Date(b.created_at);
|
||||
if (dateA < dateB) { return -1; }
|
||||
if (dateA > dateB) { return 1; }
|
||||
return 0;
|
||||
};
|
||||
const commentFragment = gql`
|
||||
fragment CoralEmbedStream_Stream_comment on Comment {
|
||||
id
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
replyCount(excludeIgnored: $excludeIgnored)
|
||||
replies(excludeIgnored: $excludeIgnored) {
|
||||
nodes {
|
||||
id
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const descending = (a, b) => ascending(a, b) * -1;
|
||||
|
||||
const LOAD_COMMENT_COUNTS_QUERY = gql`
|
||||
query CoralEmbedStream_LoadCommentCounts($assetUrl: String, , $commentId: ID!, $assetId: ID, $hasComment: Boolean!, $excludeIgnored: Boolean) {
|
||||
comment(id: $commentId) @include(if: $hasComment) {
|
||||
id
|
||||
const COMMENTS_ADDED_SUBSCRIPTION = gql`
|
||||
subscription onCommentAdded($assetId: ID!, $excludeIgnored: Boolean){
|
||||
commentAdded(asset_id: $assetId){
|
||||
parent {
|
||||
id
|
||||
replyCount(excludeIgnored: $excludeIgnored)
|
||||
}
|
||||
replyCount(excludeIgnored: $excludeIgnored)
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
asset(id: $assetId, url: $assetUrl) {
|
||||
}
|
||||
${commentFragment}
|
||||
`;
|
||||
|
||||
const COMMENTS_EDITED_SUBSCRIPTION = gql`
|
||||
subscription onCommentEdited($assetId: ID!){
|
||||
commentEdited(asset_id: $assetId){
|
||||
id
|
||||
commentCount(excludeIgnored: $excludeIgnored)
|
||||
comments(limit: 10) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
id
|
||||
replyCount(excludeIgnored: $excludeIgnored)
|
||||
}
|
||||
body
|
||||
status
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,24 +225,6 @@ const LOAD_MORE_QUERY = gql`
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const commentFragment = gql`
|
||||
fragment CoralEmbedStream_Stream_comment on Comment {
|
||||
id
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
replyCount(excludeIgnored: $excludeIgnored)
|
||||
replies {
|
||||
nodes {
|
||||
id
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const fragments = {
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Stream_root on RootQuery {
|
||||
@@ -316,6 +282,7 @@ const fragments = {
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
refetching: state.embed.refetching,
|
||||
commentCountCache: state.stream.commentCountCache,
|
||||
activeReplyBox: state.stream.activeReplyBox,
|
||||
commentId: state.stream.commentId,
|
||||
@@ -331,7 +298,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||
addNotification,
|
||||
setActiveReplyBox,
|
||||
editName,
|
||||
setCommentCountCache,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
|
||||
@@ -91,6 +91,9 @@ const extension = {
|
||||
edited
|
||||
editableUntil
|
||||
}
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
@@ -144,6 +147,9 @@ const extension = {
|
||||
tags,
|
||||
status: null,
|
||||
replyCount: 0,
|
||||
parent: parent_id
|
||||
? {id: parent_id}
|
||||
: null,
|
||||
replies: {
|
||||
__typename: 'CommentConnection',
|
||||
nodes: [],
|
||||
@@ -165,7 +171,7 @@ const extension = {
|
||||
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
|
||||
return prev;
|
||||
}
|
||||
return insertCommentIntoEmbedQuery(prev, parent_id, comment);
|
||||
return insertCommentIntoEmbedQuery(prev, comment);
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import update from 'immutability-helper';
|
||||
|
||||
function findAndInsertComment(parent, id, comment) {
|
||||
const [connectionField, countField, action] = parent.comments
|
||||
function applyToCommentsOrigin(root, callback) {
|
||||
if (root.comment) {
|
||||
if (root.comment.parent) {
|
||||
return update(root, {
|
||||
comment: {
|
||||
parent: {
|
||||
$apply: (node) => callback(node),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return update(root, {
|
||||
comment: {
|
||||
$apply: (node) => callback(node),
|
||||
},
|
||||
});
|
||||
}
|
||||
return update(root, {
|
||||
asset: {$apply: (asset) => callback(asset)},
|
||||
});
|
||||
}
|
||||
|
||||
function findAndInsertComment(parent, comment) {
|
||||
const [connectionField, countField, action] = parent.__typename === 'Asset'
|
||||
? ['comments', 'commentCount', '$unshift']
|
||||
: ['replies', 'replyCount', '$push'];
|
||||
|
||||
if (!id || parent.id === id) {
|
||||
if (!comment.parent || parent.id === comment.parent.id) {
|
||||
return update(parent, {
|
||||
[connectionField]: {
|
||||
nodes: {[action]: [comment]},
|
||||
@@ -21,13 +43,13 @@ function findAndInsertComment(parent, id, comment) {
|
||||
[connectionField]: {
|
||||
nodes: {
|
||||
$apply: (nodes) =>
|
||||
nodes.map((node) => findAndInsertComment(node, id, comment))
|
||||
nodes.map((node) => findAndInsertComment(node, comment))
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function insertCommentIntoEmbedQuery(root, id, comment) {
|
||||
export function insertCommentIntoEmbedQuery(root, comment) {
|
||||
|
||||
// Increase total comment count by one.
|
||||
root = update(root, {
|
||||
@@ -35,30 +57,11 @@ export function insertCommentIntoEmbedQuery(root, id, comment) {
|
||||
totalCommentCount: {$apply: (c) => c + 1},
|
||||
},
|
||||
});
|
||||
|
||||
if (root.comment) {
|
||||
if (root.comment.parent) {
|
||||
return update(root, {
|
||||
comment: {
|
||||
parent: {
|
||||
$apply: (node) => findAndInsertComment(node, id, comment),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return update(root, {
|
||||
comment: {
|
||||
$apply: (node) => findAndInsertComment(node, id, comment),
|
||||
},
|
||||
});
|
||||
}
|
||||
return update(root, {
|
||||
asset: {$apply: (asset) => findAndInsertComment(asset, id, comment)},
|
||||
});
|
||||
return applyToCommentsOrigin(root, (origin) => findAndInsertComment(origin, comment));
|
||||
}
|
||||
|
||||
function findAndRemoveComment(parent, id) {
|
||||
const [connectionField, countField] = parent.comments
|
||||
const [connectionField, countField] = parent.__typename === 'Asset'
|
||||
? ['comments', 'commentCount']
|
||||
: ['replies', 'replyCount'];
|
||||
|
||||
@@ -78,7 +81,7 @@ function findAndRemoveComment(parent, id) {
|
||||
};
|
||||
|
||||
if (parent[countField] && next.length !== connection.nodes.length) {
|
||||
changes[countField] = {$set: changes[countField] - 1};
|
||||
changes[countField] = {$set: parent[countField] - 1};
|
||||
}
|
||||
return update(parent, changes);
|
||||
}
|
||||
@@ -91,24 +94,88 @@ export function removeCommentFromEmbedQuery(root, id) {
|
||||
totalCommentCount: {$apply: (c) => c - 1},
|
||||
},
|
||||
});
|
||||
return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id));
|
||||
}
|
||||
|
||||
if (root.comment) {
|
||||
if (root.comment.parent) {
|
||||
return update(root, {
|
||||
comment: {
|
||||
parent: {
|
||||
$apply: (node) => findAndRemoveComment(node, id),
|
||||
},
|
||||
},
|
||||
});
|
||||
function findComment(nodes, callback) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (callback(node)) {
|
||||
return node;
|
||||
}
|
||||
return update(root, {
|
||||
comment: {
|
||||
$apply: (node) => findAndRemoveComment(node, id),
|
||||
if (node.replies) {
|
||||
const find = findComment(node.replies.nodes, callback);
|
||||
if (find){
|
||||
return find;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function findCommentInEmbedQuery(root, callbackOrId) {
|
||||
let callback = callbackOrId;
|
||||
if (typeof callbackOrId === 'string') {
|
||||
callback = (node) => node.id === callbackOrId;
|
||||
}
|
||||
if (root.comment) {
|
||||
if (callback(root.comment)) {
|
||||
return root.comment;
|
||||
}
|
||||
if (root.comment.parent && callback(root.comment.parent)) {
|
||||
return root.comment.parent;
|
||||
}
|
||||
}
|
||||
if (!root.asset.comments) {
|
||||
return false;
|
||||
}
|
||||
return findComment(root.asset.comments.nodes, callback);
|
||||
}
|
||||
|
||||
const ascending = (a, b) => {
|
||||
const dateA = new Date(a.created_at);
|
||||
const dateB = new Date(b.created_at);
|
||||
if (dateA < dateB) { return -1; }
|
||||
if (dateA > dateB) { return 1; }
|
||||
return 0;
|
||||
};
|
||||
|
||||
function findAndInsertFetchedComments(parent, comments, parent_id) {
|
||||
const isAsset = parent.__typename === 'Asset';
|
||||
const connectionField = isAsset ? 'comments' : 'replies';
|
||||
if (!parent_id && connectionField === 'comments' || parent.id === parent_id) {
|
||||
return update(parent, {
|
||||
[connectionField]: {
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
nodes: {$apply: (nodes) => {
|
||||
if (isAsset) {
|
||||
return nodes.concat(comments.nodes);
|
||||
}
|
||||
return nodes
|
||||
.concat(comments.nodes.filter(
|
||||
(comment) => !nodes.some((node) => node.id === comment.id)
|
||||
))
|
||||
.sort(ascending);
|
||||
}},
|
||||
},
|
||||
});
|
||||
}
|
||||
return update(root, {
|
||||
asset: {$apply: (asset) => findAndRemoveComment(asset, id)},
|
||||
|
||||
const connection = parent[connectionField];
|
||||
if (!connection) {
|
||||
return parent;
|
||||
}
|
||||
return update(parent, {
|
||||
[connectionField]: {
|
||||
nodes: {
|
||||
$apply: (nodes) =>
|
||||
nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id))
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) {
|
||||
return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id));
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ import {checkLogin} from 'coral-framework/actions/auth';
|
||||
import './graphql';
|
||||
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
|
||||
import {getStore, injectReducers} from 'coral-framework/services/store';
|
||||
import {client} from 'coral-framework/services/client';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
import AppRouter from './AppRouter';
|
||||
import {pym} from 'coral-framework';
|
||||
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
|
||||
import reducers from './reducers';
|
||||
|
||||
const store = getStore();
|
||||
const client = getClient();
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as actions from '../constants/embed';
|
||||
const initialState = {
|
||||
activeTab: 'stream',
|
||||
previousTab: '',
|
||||
refetching: false,
|
||||
};
|
||||
|
||||
export default function stream(state = initialState, action) {
|
||||
@@ -13,6 +14,22 @@ export default function stream(state = initialState, action) {
|
||||
activeTab: action.tab,
|
||||
previousTab: state.activeTab,
|
||||
};
|
||||
case 'APOLLO_QUERY_INIT':
|
||||
if (action.queryString.indexOf('query CoralEmbedStream_Embed(') >= 0) {
|
||||
return {
|
||||
...state,
|
||||
refetching: action.isRefetch,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
case 'APOLLO_QUERY_RESULT':
|
||||
if (action.operationName === 'CoralEmbedStream_Embed') {
|
||||
return {
|
||||
...state,
|
||||
refetching: false,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import {networkInterface} from './transport';
|
||||
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
|
||||
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
let client;
|
||||
|
||||
// TODO: replace absolute reference with something loaded from the store/page.
|
||||
// const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', {
|
||||
// reconnect: true
|
||||
// });
|
||||
// const networkInterface = addGraphQLSubscriptions(
|
||||
// getNetworkInterface(),
|
||||
// wsClient,
|
||||
// );
|
||||
export const client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface
|
||||
});
|
||||
export function getClient() {
|
||||
if (client) {
|
||||
return client;
|
||||
}
|
||||
|
||||
export default client;
|
||||
const wsClient = new SubscriptionClient(`ws://${location.host}/api/v1/live`, {
|
||||
reconnect: true
|
||||
});
|
||||
|
||||
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
|
||||
networkInterface,
|
||||
wsClient,
|
||||
);
|
||||
|
||||
client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface: networkInterfaceWithSubscriptions,
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,7 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {client} from './client';
|
||||
|
||||
const apolloErrorReporter = () => (next) => (action) => {
|
||||
if (action.type === 'APOLLO_QUERY_ERROR') {
|
||||
console.error(action.error);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(
|
||||
client.middleware(),
|
||||
thunk,
|
||||
apolloErrorReporter,
|
||||
),
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
import {getClient} from './client';
|
||||
|
||||
export function injectReducers(reducers) {
|
||||
const store = getStore();
|
||||
@@ -35,9 +14,30 @@ export function getStore() {
|
||||
return window.coralStore;
|
||||
}
|
||||
|
||||
const apolloErrorReporter = () => (next) => (action) => {
|
||||
if (action.type === 'APOLLO_QUERY_ERROR') {
|
||||
console.error(action.error);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(
|
||||
getClient().middleware(),
|
||||
thunk,
|
||||
apolloErrorReporter,
|
||||
),
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
const coralReducers = {
|
||||
...mainReducer,
|
||||
apollo: client.reducer()
|
||||
apollo: getClient().reducer()
|
||||
};
|
||||
|
||||
window.coralStore = createStore(
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import {print} from 'graphql-tag/printer';
|
||||
|
||||
// quick way to add the subscribe and unsubscribe functions to the network interface
|
||||
const addGraphQLSubscriptions = (networkInterface, wsClient) => {
|
||||
return Object.assign(networkInterface, {
|
||||
subscribe: (request, handler) => wsClient.subscribe({
|
||||
query: print(request.query),
|
||||
variables: request.variables,
|
||||
}, handler),
|
||||
unsubscribe: (id) => {
|
||||
wsClient.unsubscribe(id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default addGraphQLSubscriptions;
|
||||
@@ -36,18 +36,10 @@ class CommentBox extends React.Component {
|
||||
}
|
||||
};
|
||||
}
|
||||
static get defaultProps() {
|
||||
return {
|
||||
setCommentCountCache: () => {}
|
||||
};
|
||||
}
|
||||
postComment = ({body}) => {
|
||||
const {
|
||||
commentPostedHandler,
|
||||
postComment,
|
||||
setCommentCountCache,
|
||||
commentCountCache,
|
||||
isReply,
|
||||
assetId,
|
||||
parentId,
|
||||
addNotification,
|
||||
@@ -60,8 +52,6 @@ class CommentBox extends React.Component {
|
||||
...this.props.commentBox
|
||||
};
|
||||
|
||||
!isReply && setCommentCountCache(commentCountCache + 1);
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.state.hooks.preSubmit.forEach((hook) => hook());
|
||||
|
||||
@@ -74,19 +64,12 @@ class CommentBox extends React.Component {
|
||||
|
||||
notifyForNewCommentStatus(addNotification, postedComment.status);
|
||||
|
||||
if (postedComment.status === 'REJECTED') {
|
||||
!isReply && setCommentCountCache(commentCountCache);
|
||||
} else if (postedComment.status === 'PREMOD') {
|
||||
!isReply && setCommentCountCache(commentCountCache);
|
||||
}
|
||||
|
||||
if (commentPostedHandler) {
|
||||
commentPostedHandler();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
!isReply && setCommentCountCache(commentCountCache);
|
||||
});
|
||||
|
||||
this.setState({postedCount: this.state.postedCount + 1});
|
||||
@@ -190,7 +173,6 @@ CommentBox.propTypes = {
|
||||
authorId: PropTypes.string.isRequired,
|
||||
isReply: PropTypes.bool.isRequired,
|
||||
canPost: PropTypes.bool,
|
||||
setCommentCountCache: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
@@ -23,7 +23,7 @@ class TabBar extends React.Component {
|
||||
.map((child, tabId) =>
|
||||
React.cloneElement(child, {
|
||||
tabId,
|
||||
active: tabId === activeTab,
|
||||
active: child.props.id === activeTab,
|
||||
onTabClick: this.handleClickTab,
|
||||
cStyle
|
||||
})
|
||||
|
||||
@@ -340,6 +340,12 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
|
||||
// Execute the edit.
|
||||
const comment = await CommentsService.edit(id, context.user.id, {body, status});
|
||||
|
||||
if (context.pubsub) {
|
||||
|
||||
// Publish the edited comment via the subscription.
|
||||
context.pubsub.publish('commentEdited', comment);
|
||||
}
|
||||
|
||||
return comment;
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const Comment = {
|
||||
},
|
||||
async editing(comment, _, {loaders: {Settings}}) {
|
||||
const settings = await Settings.load();
|
||||
const editableUntil = new Date(Number(comment.created_at) + settings.editCommentWindowLength);
|
||||
const editableUntil = new Date(Number(new Date(comment.created_at)) + settings.editCommentWindowLength);
|
||||
return {
|
||||
edited: comment.edited,
|
||||
editableUntil: editableUntil
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const Subscription = {
|
||||
commentAdded(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentEdited(comment) {
|
||||
return comment;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
|
||||
filter: (comment) => comment.asset_id === args.asset_id
|
||||
},
|
||||
}),
|
||||
commentEdited: (options, args) => ({
|
||||
commentEdited: {
|
||||
filter: (comment) => comment.asset_id === args.asset_id
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -883,6 +883,7 @@ type RootMutation {
|
||||
|
||||
type Subscription {
|
||||
commentAdded(asset_id: ID!): Comment
|
||||
commentEdited(asset_id: ID!): Comment
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
+3
-2
@@ -202,7 +202,7 @@ en:
|
||||
flag_reason: "Reason for reporting (Optional)"
|
||||
flag_username: "Report username"
|
||||
framework:
|
||||
banned_account_msg: "Your account is currently suspended. This means that you cannot Like Report or write comments. Please contact us if you have any questions."
|
||||
banned_account_msg: "Your account is currently banned. This means that you cannot Like, Report, or write comments. Please contact us if you have any questions."
|
||||
because_you_ignored: "Because you ignored the following commenters, their comments are hidden."
|
||||
comment: comment
|
||||
comment_is_ignored: "This comment is hidden because you ignored this user."
|
||||
@@ -224,6 +224,7 @@ en:
|
||||
success_bio_update: "Your biography has been updated"
|
||||
success_name_update: "Your username has been updated"
|
||||
success_update_settings: "The changes you have made have been applied to the comment stream on this article"
|
||||
view_all_replies_unknown_number: "view all replies"
|
||||
view_all_replies: "view {0} replies"
|
||||
view_all_replies_initial: "view all {0} replies"
|
||||
view_more_comments: "view more comments"
|
||||
@@ -322,7 +323,7 @@ en:
|
||||
bio: bio
|
||||
cancel: "Cancel"
|
||||
days: "{0} days"
|
||||
description_0: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}."
|
||||
description_0: "Would you like to temporarily suspend this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}."
|
||||
description_1: "Suspending this user will temporarily disable their account and hide all of their comments on the site."
|
||||
description_notify: "Suspending this user will temporarily disable their account and hide all of their comments on the site."
|
||||
description_reject: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}."
|
||||
|
||||
@@ -223,6 +223,7 @@ es:
|
||||
success_bio_update: "Tu biografia fue actualizada"
|
||||
success_name_update: "Tu nombre de usuario ha sido actualizado"
|
||||
success_update_settings: "La configuración de este articulo fue actualizada"
|
||||
view_all_replies_unknown_number: "ver todas las respuestas"
|
||||
view_all_replies: "ver {0} respuestas"
|
||||
view_all_replies_initial: "ver todas las {0} respuestas"
|
||||
view_more_comments: "Ver más comentarios"
|
||||
|
||||
+4
-1
@@ -104,7 +104,10 @@ const CommentSchema = new Schema({
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
},
|
||||
toJSON: {
|
||||
virtuals: true,
|
||||
},
|
||||
});
|
||||
|
||||
CommentSchema.virtual('edited').get(function() {
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"react-apollo": "^1.1.0",
|
||||
"react-recaptcha": "^2.2.6",
|
||||
"react-toastify": "^1.5.0",
|
||||
"react-transition-group": "^1.1.3",
|
||||
"recompose": "^0.23.1",
|
||||
"redis": "^2.7.1",
|
||||
"resolve": "^1.3.2",
|
||||
|
||||
@@ -9,10 +9,10 @@ const UserBox = ({loggedIn, user, logout, onShowProfile}) => (
|
||||
<div>
|
||||
{
|
||||
loggedIn ? (
|
||||
<div className={styles.userBox}>
|
||||
<div className={`${styles.userBox} talk-stream-auth-userbox`}>
|
||||
<span className={styles.userBoxLoggedIn}>{t('sign_in.logged_in_as')}</span>
|
||||
<a onClick={onShowProfile}>{user.username}</a>. {t('sign_in.not_you')}
|
||||
<a className={styles.logout} onClick={() => logout()}>
|
||||
<a className={`${styles.logout} talk-stream-userbox-logout`} onClick={() => logout()}>
|
||||
{t('sign_in.logout')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@ import cn from 'classnames';
|
||||
|
||||
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
|
||||
|
||||
const name = 'coral-plugin-respect';
|
||||
|
||||
class RespectButton extends Component {
|
||||
|
||||
handleClick = () => {
|
||||
@@ -47,13 +49,13 @@ class RespectButton extends Component {
|
||||
let count = getTotalActionCount('RespectActionSummary', comment);
|
||||
|
||||
return (
|
||||
<div className={styles.respect}>
|
||||
<div className={cn(styles.respect, `${name}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.respected]: myRespect})}
|
||||
className={cn(styles.button, {[styles.respected]: myRespect}, `${name}-button`)}
|
||||
onClick={this.handleClick} >
|
||||
<span>{t(myRespect ? 'respected' : 'respect')}</span>
|
||||
<Icon className={cn(styles.icon, {[styles.respected]: myRespect})} />
|
||||
{count > 0 && count}
|
||||
<span className={`${name}-button-text`}>{t(myRespect ? 'respected' : 'respect')}</span>
|
||||
<Icon className={cn(styles.icon, {[styles.respected]: myRespect}, `${name}-icon`)} />
|
||||
<span className={`${name}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6909,7 +6909,7 @@ react-toastify@^1.5.0:
|
||||
prop-types "^15.5.8"
|
||||
react-transition-group "^1.1.2"
|
||||
|
||||
react-transition-group@^1.1.2:
|
||||
react-transition-group@^1.1.2, react-transition-group@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.1.3.tgz#5e02cf6e44a863314ff3c68a0c826c2d9d70b221"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user