From ea3d8e141d80ad0185379db809863e0ed27b20ee Mon Sep 17 00:00:00 2001 From: David Erwin Date: Fri, 2 Jun 2017 14:22:06 -0400 Subject: [PATCH 01/48] Use config auth_token if present on http headers --- client/coral-framework/helpers/request.js | 35 +++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/client/coral-framework/helpers/request.js b/client/coral-framework/helpers/request.js index 0e15003e1..46ef8fd51 100644 --- a/client/coral-framework/helpers/request.js +++ b/client/coral-framework/helpers/request.js @@ -1,6 +1,31 @@ import bowser from 'bowser'; import * as Storage from './storage'; import merge from 'lodash/merge'; +import {getStore} from 'coral-framework/services/store'; + +/** + * getAuthToken returns the active auth token or null + * Note: this method does not have access to the cookie based token used by + * browsers that don't allow us to use cross domain iframe local storage. + * @return {string|null} + */ +const getAuthToken = () => { + let state = getStore().getState(); + + if (state.config.auth_token) { + + // if an auth_token exists in config, use it. + return state.config.auth_token; + + } else if (!bowser.safari && !bowser.ios) { + + // Use local storage auth tokens where there's a stable api. + return Storage.getItem('token'); + + } + + return null; +}; const buildOptions = (inputOptions = {}) => { const defaultOptions = { @@ -14,12 +39,10 @@ const buildOptions = (inputOptions = {}) => { let options = merge({}, defaultOptions, inputOptions); - if (!bowser.safari && !bowser.ios) { - let authorization = Storage.getItem('token'); - - if (authorization) { - options.headers.Authorization = `Bearer ${authorization}`; - } + // Apply authToken header + let authToken = getAuthToken(); + if (authToken) { + options.headers.Authorization = `Bearer ${authToken}`; } if (options.method.toLowerCase() !== 'get') { From b05b54a99174545007cad23e9938866246a00e3c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 17:46:30 +0700 Subject: [PATCH 02/48] Add auth_token default config --- views/article.ejs | 1 + 1 file changed, 1 insertion(+) diff --git a/views/article.ejs b/views/article.ejs index bc29467f1..c216f158d 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -29,6 +29,7 @@ talk: '/', asset_url: '<%= asset_url ? asset_url : '' %>', asset_id: '<%= asset_id ? asset_id : '' %>', + auth_token: '', plugin_config: { test: 'data', debug: false From defd28b20f173e78ff6efc2d946cefd0d81b9d05 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 17:46:57 +0700 Subject: [PATCH 03/48] Remove double login checks --- .../coral-plugin-auth/client/components/SignInContainer.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/coral-plugin-auth/client/components/SignInContainer.js b/plugins/coral-plugin-auth/client/components/SignInContainer.js index 6f3c460bb..6950f4124 100644 --- a/plugins/coral-plugin-auth/client/components/SignInContainer.js +++ b/plugins/coral-plugin-auth/client/components/SignInContainer.js @@ -18,7 +18,6 @@ import { facebookCallback, invalidForm, validForm, - checkLogin } from 'coral-framework/actions/auth'; class SignInContainer extends React.Component { @@ -38,10 +37,6 @@ class SignInContainer extends React.Component { }; } - componentWillMount() { - this.props.checkLogin(); - } - componentDidMount() { window.addEventListener('storage', this.handleAuth); @@ -187,7 +182,6 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators( { - checkLogin, facebookCallback, fetchSignUp, fetchSignIn, From ac9e70bc67d890f781efc5056ca2586068841c5d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 17:48:09 +0700 Subject: [PATCH 04/48] Add same getAuthToken logic to apollo transport --- client/coral-framework/helpers/request.js | 3 +-- client/coral-framework/services/transport.js | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/client/coral-framework/helpers/request.js b/client/coral-framework/helpers/request.js index 46ef8fd51..52b9ea7da 100644 --- a/client/coral-framework/helpers/request.js +++ b/client/coral-framework/helpers/request.js @@ -9,7 +9,7 @@ import {getStore} from 'coral-framework/services/store'; * browsers that don't allow us to use cross domain iframe local storage. * @return {string|null} */ -const getAuthToken = () => { +export const getAuthToken = () => { let state = getStore().getState(); if (state.config.auth_token) { @@ -21,7 +21,6 @@ const getAuthToken = () => { // Use local storage auth tokens where there's a stable api. return Storage.getItem('token'); - } return null; diff --git a/client/coral-framework/services/transport.js b/client/coral-framework/services/transport.js index 0c430a021..f11c20888 100644 --- a/client/coral-framework/services/transport.js +++ b/client/coral-framework/services/transport.js @@ -1,6 +1,5 @@ import {createNetworkInterface} from 'apollo-client'; -import * as Storage from '../helpers/storage'; -import bowser from 'bowser'; +import {getAuthToken} from '../helpers/request'; //============================================================================== // NETWORK INTERFACE @@ -23,8 +22,9 @@ networkInterface.use([{ req.options.headers = {}; // Create the header object if needed. } - if (!bowser.safari && !bowser.ios) { - req.options.headers['authorization'] = `Bearer ${Storage.getItem('token')}`; + let authToken = getAuthToken(); + if (authToken) { + req.options.headers['authorization'] = `Bearer ${authToken}`; } next(); From 7c145bbb3ceffc61064de6fd91c5b93c491597d2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 17:49:43 +0700 Subject: [PATCH 05/48] Refactor import order and framework initialization --- client/coral-admin/src/index.js | 3 ++- client/coral-admin/src/reducers/index.js | 2 +- client/coral-admin/src/services/store.js | 12 ++++++++---- client/coral-embed-stream/src/index.js | 8 ++++---- client/coral-embed-stream/src/reducers/index.js | 2 +- client/coral-framework/helpers/plugins.js | 16 ++++++++++------ client/coral-framework/reducers/index.js | 2 -- 7 files changed, 26 insertions(+), 19 deletions(-) diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index d0dfc5f9a..8cd7685f0 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -9,9 +9,10 @@ import App from './components/App'; import 'react-mdl/extra/material.js'; import './graphql'; -import {loadPluginsTranslations} from 'coral-framework/helpers/plugins'; +import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; loadPluginsTranslations(); +injectPluginsReducers(); render( diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 3036d6271..04c8b6590 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -13,5 +13,5 @@ export default { community, moderation, install, - config + config, }; diff --git a/client/coral-admin/src/services/store.js b/client/coral-admin/src/services/store.js index 2ab9d3750..9815a0f70 100644 --- a/client/coral-admin/src/services/store.js +++ b/client/coral-admin/src/services/store.js @@ -14,14 +14,18 @@ if (window.devToolsExtension) { middlewares.push(window.devToolsExtension()); } +const coralReducers = { + ...mainReducer, + apollo: client.reducer() +}; + const store = createStore( - combineReducers({ - ...mainReducer, - apollo: client.reducer() - }), + combineReducers(coralReducers), {}, compose(...middlewares) ); +store.coralReducers = coralReducers; + window.coralStore = store; export default store; diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 06d52649a..1b2d278b2 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -2,20 +2,20 @@ import React from 'react'; import {render} from 'react-dom'; import {ApolloProvider} from 'react-apollo'; -import {client} from 'coral-framework/services/client'; import {checkLogin} from 'coral-framework/actions/auth'; import './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; - -import reducers from './reducers'; import {getStore, injectReducers} from 'coral-framework/services/store'; +import {client} from 'coral-framework/services/client'; import AppRouter from './AppRouter'; import {pym} from 'coral-framework'; -import {loadPluginsTranslations} from 'coral-framework/helpers/plugins'; +import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; +import reducers from './reducers'; const store = getStore(); loadPluginsTranslations(); +injectPluginsReducers(); injectReducers(reducers); // Don't run this in the popup. diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js index 590b87eea..5ddac4567 100644 --- a/client/coral-embed-stream/src/reducers/index.js +++ b/client/coral-embed-stream/src/reducers/index.js @@ -5,5 +5,5 @@ import stream from './stream'; export default { embed, stream, - config + config, }; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 02ee8c3a7..0eaecbadd 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -7,12 +7,7 @@ import pick from 'lodash/pick'; import plugins from 'pluginsConfig'; import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; import {loadTranslations} from 'coral-framework/services/i18n'; - -export const pluginReducers = merge( - ...plugins - .filter((o) => o.module.reducer) - .map((o) => ({...o.module.reducer})) -); +import {injectReducers} from 'coral-framework/services/store'; /** * Returns React Elements for given slot. @@ -98,3 +93,12 @@ function getTranslations() { export function loadPluginsTranslations() { getTranslations().forEach((t) => loadTranslations(t)); } + +export function injectPluginsReducers() { + const reducers = merge( + ...plugins + .filter((o) => o.module.reducer) + .map((o) => ({...o.module.reducer})) + ); + injectReducers(reducers); +} diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js index d5b66bbae..8928cae93 100644 --- a/client/coral-framework/reducers/index.js +++ b/client/coral-framework/reducers/index.js @@ -2,12 +2,10 @@ import auth from './auth'; import user from './user'; import asset from './asset'; import {reducer as commentBox} from '../../coral-plugin-commentbox'; -import {pluginReducers} from '../helpers/plugins'; export default { auth, user, asset, commentBox, - ...pluginReducers }; From a9796d47cdf7dd7988547291df05c8b0c21efa81 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 17:51:06 +0700 Subject: [PATCH 06/48] Check login after loading config --- client/coral-embed-stream/src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 1b2d278b2..4dee04f1e 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -20,12 +20,11 @@ injectReducers(reducers); // Don't run this in the popup. if (!window.opener) { - store.dispatch(checkLogin()); - pym.sendMessage('getConfig'); pym.onMessage('config', (config) => { store.dispatch(addExternalConfig(JSON.parse(config))); + store.dispatch(checkLogin()); }); } From c20df65ebcee7d6ea4a5e04f6a0557d22d453ad0 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 18:21:40 +0700 Subject: [PATCH 07/48] Use same lifespan for cookies as for tokens --- package.json | 1 + services/passport.js | 3 ++- yarn.lock | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fe87f17f5..37c85ed64 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "minimist": "^1.2.0", "mongoose": "^4.9.8", "morgan": "^1.8.1", + "ms": "^2.0.0", "natural": "^0.5.0", "node-emoji": "^1.5.1", "node-fetch": "^1.6.3", diff --git a/services/passport.js b/services/passport.js index 4f5a8fb8d..8d264f67b 100644 --- a/services/passport.js +++ b/services/passport.js @@ -10,6 +10,7 @@ const uuid = require('uuid'); const debug = require('debug')('talk:passport'); const {createClient} = require('./redis'); const bowser = require('bowser'); +const ms = require('ms'); // Create a redis client to use for authentication. const client = createClient(); @@ -39,7 +40,7 @@ const SetTokenForSafari = (req, res, token) => { if (browser.ios || browser.safari) { res.cookie('authorization', token, { httpOnly: true, - expires: new Date(Date.now() + 900000) + expires: new Date(Date.now() + ms(JWT_EXPIRY)) }); } }; diff --git a/yarn.lock b/yarn.lock index 9855d91e8..92966d3b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5365,6 +5365,10 @@ ms@0.7.3, ms@^0.7.1: version "0.7.3" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" +ms@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + muri@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/muri/-/muri-1.2.1.tgz#ec7ea5ce6ca6a523eb1ab35bacda5fa816c9aa3c" From 6b0cdae183e93e6b5dbd18e68f65ff62205407fd Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 20:51:08 +0700 Subject: [PATCH 08/48] Subscribe to comments --- .../coral-embed-stream/src/actions/stream.js | 1 - .../src/components/Comment.js | 180 ++++++++++++++---- .../src/components/NewCount.js | 23 +-- .../src/components/Stream.js | 111 ++++++++++- .../src/constants/stream.js | 2 - .../src/containers/Embed.js | 11 +- .../src/containers/Stream.js | 172 +++++++++-------- .../coral-embed-stream/src/graphql/index.js | 8 +- .../coral-embed-stream/src/graphql/utils.js | 51 ++++- client/coral-framework/services/client.js | 20 +- .../coral-framework/services/subscriptions.js | 16 -- client/coral-plugin-commentbox/CommentBox.js | 18 -- graph/mutators/comment.js | 6 + graph/resolvers/comment.js | 2 +- graph/resolvers/subscription.js | 3 + graph/subscriptions.js | 5 + graph/typeDefs.graphql | 1 + models/comment.js | 5 +- 18 files changed, 418 insertions(+), 217 deletions(-) delete mode 100644 client/coral-framework/services/subscriptions.js diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index 81b57a0fe..6a9e47838 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -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]; diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 48392cc51..2c69f3979 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -26,6 +26,41 @@ 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 +72,7 @@ const ActionButton = ({children}) => { }; class Comment extends React.Component { + constructor(props) { super(props); @@ -49,7 +85,29 @@ 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, + ...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)); + } + } } static propTypes = { @@ -67,6 +125,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 +186,45 @@ class Comment extends React.Component { } } + 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 `userId` 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 +260,20 @@ 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 flagSummary = getActionSummary('FlagActionSummary', comment); const dontAgreeSummary = getActionSummary( 'DontAgreeActionSummary', @@ -369,46 +468,45 @@ class Comment extends React.Component { assetId={asset.id} /> : null} - {comment.replies && - comment.replies.nodes.map((reply) => { - return commentIsIgnored(reply) - ? - : ; - })} - {comment.replies && -
- comment.replies.nodes.length} - loadMore={() => loadMore(comment.id)} - /> -
} + {view.map((reply) => { + return commentIsIgnored(reply) + ? + : ; + })} +
+ view.length} + loadMore={this.loadNewReplies} + /> +
); } diff --git a/client/coral-embed-stream/src/components/NewCount.js b/client/coral-embed-stream/src/components/NewCount.js index 43dce13e0..1c135ab9b 100644 --- a/client/coral-embed-stream/src/components/NewCount.js +++ b/client/coral-embed-stream/src/components/NewCount.js @@ -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
{ - props.commentCountCache && newComments > 0 ? - : 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, }; diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 11f2b7819..725d5c488 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -1,6 +1,5 @@ import React, {PropTypes} from 'react'; import LoadMore from './LoadMore'; -import NewCount from './NewCount'; import Comment from '../containers/Comment'; import SuspendedAccount from './SuspendedAccount'; @@ -13,9 +12,81 @@ 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'; +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 +95,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 +133,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 +192,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 +232,15 @@ class Stream extends React.Component { charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={this.props.editComment} + liveUpdates={true} /> :
- {comments && comments.nodes.map((comment) => { + {view.map((comment) => { return commentIsIgnored(comment) ? : ; })}
diff --git a/client/coral-embed-stream/src/constants/stream.js b/client/coral-embed-stream/src/constants/stream.js index cb17edb2f..4be4dc125 100644 --- a/client/coral-embed-stream/src/constants/stream.js +++ b/client/coral-embed-stream/src/constants/stream.js @@ -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'; diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 05dd9fb91..74d4e9d70 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -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,13 +33,6 @@ 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); - } } } @@ -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 ); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index f2f5f67db..ea42b5bd6 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -2,7 +2,7 @@ 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, @@ -11,23 +11,68 @@ 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 {getDefinitionName} from 'coral-framework/utils'; +import {findCommentInEmbedQuery, insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} 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, + subscribeToUpdates = () => { + this.props.data.subscribeToMore({ + document: COMMENTS_EDITED_SUBSCRIPTION, + variables: { + assetId: this.props.root.asset.id, + }, + updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => { - // Apollo requires this, even though we don't use it... - updateQuery: (data) => data, + // 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); + } + }, + }); + 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); + } }); }; @@ -95,38 +140,6 @@ class StreamContainer extends React.Component { }); } - 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, @@ -157,15 +170,7 @@ class StreamContainer extends React.Component { }; componentDidMount() { - if (this.props.previousTab) { - this.props.data.refetch() - .then(({data: {asset: {commentCount}}}) => { - return this.props.setCommentCountCache(commentCount); - }); - } - this.countPoll = setInterval(() => { - this.getCounts(this.props.data.variables); - }, NEW_COMMENT_COUNT_POLL_INTERVAL); + this.subscribeToUpdates(); } componentWillUnmount() { @@ -177,7 +182,6 @@ class StreamContainer extends React.Component { {...this.props} loadMore={this.loadMore} loadMoreComments={this.loadMoreComments} - loadNewComments={this.loadNewComments} loadNewReplies={this.loadNewReplies} />; } @@ -191,26 +195,47 @@ const ascending = (a, b) => { return 0; }; -const descending = (a, b) => ascending(a, b) * -1; +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 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 +266,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 { @@ -331,7 +338,6 @@ const mapDispatchToProps = (dispatch) => addNotification, setActiveReplyBox, editName, - setCommentCountCache, }, dispatch); export default compose( diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 1b4ac500b..04225eb96 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -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); }, } }), diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 163432479..3ab9c9e3b 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -1,11 +1,11 @@ import update from 'immutability-helper'; -function findAndInsertComment(parent, id, comment) { +function findAndInsertComment(parent, comment) { const [connectionField, countField, action] = parent.comments ? ['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 +21,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, { @@ -41,19 +41,19 @@ export function insertCommentIntoEmbedQuery(root, id, comment) { return update(root, { comment: { parent: { - $apply: (node) => findAndInsertComment(node, id, comment), + $apply: (node) => findAndInsertComment(node, comment), }, }, }); } return update(root, { comment: { - $apply: (node) => findAndInsertComment(node, id, comment), + $apply: (node) => findAndInsertComment(node, comment), }, }); } return update(root, { - asset: {$apply: (asset) => findAndInsertComment(asset, id, comment)}, + asset: {$apply: (asset) => findAndInsertComment(asset, comment)}, }); } @@ -78,7 +78,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); } @@ -112,3 +112,38 @@ export function removeCommentFromEmbedQuery(root, id) { asset: {$apply: (asset) => findAndRemoveComment(asset, id)}, }); } + +function findComment(nodes, callback) { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (callback(node)) { + return node; + } + 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); +} diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index c77bc9848..fd906c75e 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -1,16 +1,16 @@ import ApolloClient, {addTypename} from 'apollo-client'; import {networkInterface} from './transport'; +import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws'; -// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws'; +const wsClient = new SubscriptionClient(`ws://${location.host}/api/v1/live`, { + reconnect: true +}); + +const networkInterfaceWithSubscriptions = addGraphQLSubscriptions( + networkInterface, + wsClient, +); -// 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, @@ -21,7 +21,7 @@ export const client = new ApolloClient({ } return null; }, - networkInterface + networkInterface: networkInterfaceWithSubscriptions, }); export default client; diff --git a/client/coral-framework/services/subscriptions.js b/client/coral-framework/services/subscriptions.js deleted file mode 100644 index 818a6fb33..000000000 --- a/client/coral-framework/services/subscriptions.js +++ /dev/null @@ -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; diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 3cd48d2b0..5105667ed 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -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}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 7d234c755..88c391078 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -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; }; diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 2e8412169..33ec0ef60 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -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 diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js index b3f5e655c..a593c1eb6 100644 --- a/graph/resolvers/subscription.js +++ b/graph/resolvers/subscription.js @@ -1,6 +1,9 @@ const Subscription = { commentAdded(comment) { return comment; + }, + commentEdited(comment) { + return comment; } }; diff --git a/graph/subscriptions.js b/graph/subscriptions.js index 2eb676cc4..0442c237d 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -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 + }, + }), }); /** diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index aed610ad7..187e5bba5 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -883,6 +883,7 @@ type RootMutation { type Subscription { commentAdded(asset_id: ID!): Comment + commentEdited(asset_id: ID!): Comment } ################################################################################ diff --git a/models/comment.js b/models/comment.js index 27a2bfae3..1ffeb3fd6 100644 --- a/models/comment.js +++ b/models/comment.js @@ -104,7 +104,10 @@ const CommentSchema = new Schema({ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' - } + }, + toJSON: { + virtuals: true, + }, }); CommentSchema.virtual('edited').get(function() { From 7c66d846c29f659f6238860292b7dbaa7defd495 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Mon, 5 Jun 2017 13:38:06 -0400 Subject: [PATCH 09/48] Add auth token comment --- client/coral-embed/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 570a63aed..65e77ad1d 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -147,6 +147,7 @@ function configurePymParent(pymParent, opts) { * @param {String} [opts.title] - Title of Stream (rendered in iframe) * @param {String} [opts.asset_url] - Asset URL * @param {String} [opts.asset_id] - Asset ID + * @param {String} [opts.auth_token] - (optional) A jwt representing the session */ Talk.render = function(el, opts) { if (!el) { From 9993350b7ee6ec05b2e14c7c272bbdaf840a6a8f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 00:54:12 +0700 Subject: [PATCH 10/48] Animate flash for new comments --- .../src/components/Comment.css | 10 +++++ .../src/components/Comment.js | 39 ++++++++++++++++--- .../src/components/Stream.js | 7 ++-- .../src/containers/Embed.js | 2 +- package.json | 1 + yarn.lock | 2 +- 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 5fab57d95..b0fca75c8 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -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; +} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 2c69f3979..c853e5018 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -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,7 +22,6 @@ 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'; @@ -85,9 +87,9 @@ 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) { @@ -110,6 +112,28 @@ class Comment extends React.Component { } } + componentWillAppear(callback) { + callback(); + } + 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}); + } + componentWillLeave(callback) { + callback(); + } + static propTypes = { reactKey: PropTypes.string.isRequired, @@ -325,7 +349,7 @@ class Comment extends React.Component { return (
@@ -362,17 +386,17 @@ class Comment extends React.Component { (comment.user.id === currentUser.id)) /* User can edit/delete their own comment for a short window after posting */ - ? + ? { commentIsStillEditable(comment) && Edit } /* TopRightMenu allows currentUser to ignore other users' comments */ - : + : : null} + + {view.map((reply) => { return commentIsIgnored(reply) ? @@ -499,6 +525,7 @@ class Comment extends React.Component { comment={reply} />; })} +
nodes.some((node) => node.id === id); @@ -239,7 +240,7 @@ class Stream extends React.Component { count={comments.nodes.length - view.length} loadMore={this.viewNewComments} /> -
+ {view.map((comment) => { return commentIsIgnored(comment) ? @@ -273,7 +274,7 @@ class Stream extends React.Component { liveUpdates={false} />; })} -
+ pym.scrollParentToChildEl('coralStream'), 0); diff --git a/package.json b/package.json index fe87f17f5..c3c08a2db 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 9855d91e8..6a069df84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: From 90d7e3706c267f0a1ba3f9bbd13b368d20a4c618 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Mon, 5 Jun 2017 13:58:13 -0400 Subject: [PATCH 11/48] Improve auth token logic. --- client/coral-framework/helpers/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-framework/helpers/request.js b/client/coral-framework/helpers/request.js index 52b9ea7da..57e3a6cde 100644 --- a/client/coral-framework/helpers/request.js +++ b/client/coral-framework/helpers/request.js @@ -40,7 +40,7 @@ const buildOptions = (inputOptions = {}) => { // Apply authToken header let authToken = getAuthToken(); - if (authToken) { + if (authToken !== null) { options.headers.Authorization = `Bearer ${authToken}`; } From 3ad17f008feed884a410c95fbe27a5ab759ec751 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 01:44:17 +0700 Subject: [PATCH 12/48] Refactor fetchMore updaters --- .../src/components/Comment.js | 2 + .../src/containers/Stream.js | 75 ++--------- .../coral-embed-stream/src/graphql/utils.js | 119 +++++++++++------- 3 files changed, 87 insertions(+), 109 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index c853e5018..daefc4c4e 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -115,6 +115,7 @@ class Comment extends React.Component { componentWillAppear(callback) { callback(); } + componentWillEnter(callback) { callback(); const userId = this.props.currentUser ? this.props.currentUser.id : null; @@ -130,6 +131,7 @@ class Comment extends React.Component { } this.setState({animateEnter: true}); } + componentWillLeave(callback) { callback(); } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index ea42b5bd6..fa21c1ed2 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -7,7 +7,6 @@ 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'; @@ -16,7 +15,12 @@ import Stream from '../components/Stream'; import Comment from './Comment'; import {withFragments} from 'coral-framework/hocs'; import {getDefinitionName} from 'coral-framework/utils'; -import {findCommentInEmbedQuery, insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from '../graphql/utils'; +import { + findCommentInEmbedQuery, + insertCommentIntoEmbedQuery, + removeCommentFromEmbedQuery, + insertFetchedCommentsIntoEmbedQuery, +} from '../graphql/utils'; const {showSignInDialog} = authActions; const {addNotification} = notificationActions; @@ -92,50 +96,7 @@ 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); }, }); } @@ -152,19 +113,7 @@ 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); }, }); }; @@ -187,14 +136,6 @@ class StreamContainer extends React.Component { } } -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 diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 3ab9c9e3b..d2a557f23 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -1,7 +1,29 @@ import update from 'immutability-helper'; +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.comments + const [connectionField, countField, action] = parent.__typename === 'Asset' ? ['comments', 'commentCount', '$unshift'] : ['replies', 'replyCount', '$push']; @@ -35,30 +57,11 @@ export function insertCommentIntoEmbedQuery(root, comment) { totalCommentCount: {$apply: (c) => c + 1}, }, }); - - if (root.comment) { - if (root.comment.parent) { - return update(root, { - comment: { - parent: { - $apply: (node) => findAndInsertComment(node, comment), - }, - }, - }); - } - return update(root, { - comment: { - $apply: (node) => findAndInsertComment(node, comment), - }, - }); - } - return update(root, { - asset: {$apply: (asset) => findAndInsertComment(asset, 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']; @@ -91,26 +94,7 @@ export function removeCommentFromEmbedQuery(root, id) { totalCommentCount: {$apply: (c) => c - 1}, }, }); - - if (root.comment) { - if (root.comment.parent) { - return update(root, { - comment: { - parent: { - $apply: (node) => findAndRemoveComment(node, id), - }, - }, - }); - } - return update(root, { - comment: { - $apply: (node) => findAndRemoveComment(node, id), - }, - }); - } - return update(root, { - asset: {$apply: (asset) => findAndRemoveComment(asset, id)}, - }); + return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id)); } function findComment(nodes, callback) { @@ -147,3 +131,54 @@ export function findCommentInEmbedQuery(root, callbackOrId) { } 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); + }}, + }, + }); + } + + 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) { + if (!comments.nodes.length) { + return root; + } + return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id)); +} From 2be73d1df25af1a75ec6a54eb146838c65437103 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 02:10:56 +0700 Subject: [PATCH 13/48] Unsubscribe on unmount --- .../src/containers/Stream.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index fa21c1ed2..893c15dc1 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -26,8 +26,10 @@ const {showSignInDialog} = authActions; const {addNotification} = notificationActions; class StreamContainer extends React.Component { - subscribeToUpdates = () => { - this.props.data.subscribeToMore({ + subscriptions = []; + + subscribeToUpdates() { + const sub1 = this.props.data.subscribeToMore({ document: COMMENTS_EDITED_SUBSCRIPTION, variables: { assetId: this.props.root.asset.id, @@ -50,7 +52,8 @@ class StreamContainer extends React.Component { } }, }); - this.props.data.subscribeToMore({ + + const sub2 = this.props.data.subscribeToMore({ document: COMMENTS_ADDED_SUBSCRIPTION, variables: { assetId: this.props.root.asset.id, @@ -78,7 +81,14 @@ class StreamContainer extends React.Component { 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 @@ -123,6 +133,7 @@ class StreamContainer extends React.Component { } componentWillUnmount() { + this.unsubscribe(); clearInterval(this.countPoll); } From 2d0eec15c185f6833c64c21db3d879eb92ddf729 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 02:25:32 +0700 Subject: [PATCH 14/48] Readd falsely deleted part --- client/coral-embed-stream/src/containers/Stream.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 893c15dc1..593fa1eb0 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -129,6 +129,9 @@ class StreamContainer extends React.Component { }; componentDidMount() { + if (this.props.previousTab) { + this.props.data.refetch(); + } this.subscribeToUpdates(); } From 056ddc7010bb3ac64832aae559ae76bae53fc04a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 02:25:42 +0700 Subject: [PATCH 15/48] Meaningful comment --- client/coral-embed-stream/src/components/Comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index daefc4c4e..e99755f9c 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -224,7 +224,7 @@ class Comment extends React.Component { }; // getVisibileReplies returns a list containing comments - // which were authored by `userId` or comes before the `idCursor`. + // 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]; From 32fe2918471e6f90ddd05d1b478c6ab7e4d5e856 Mon Sep 17 00:00:00 2001 From: StephanieDClark Date: Mon, 5 Jun 2017 12:37:13 -0700 Subject: [PATCH 16/48] Add classes to tabbar, tab --- client/coral-embed-stream/src/components/Embed.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 3c94d5f81..ecb10a3dd 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -41,10 +41,10 @@ export default class Embed extends React.Component { return (
- - - {t('framework.my_profile')} - {t('framework.configure_stream')} + + + {t('framework.my_profile')} + {t('framework.configure_stream')} {commentId && + + {`${selectedIds.length} comments selected`} +
+ ) + } +
{ nodes.map((comment, i) => { const status = comment.action_summaries ? 'FLAGGED' : comment.status; + const selected = selectedIds.indexOf(comment.id) !== -1; return ; diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index e7bea9430..d278e0ed1 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -185,10 +185,6 @@ span { padding: 0 14px; } - &:hover { - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); - } - &:last-child { border-bottom: none; } @@ -291,7 +287,6 @@ span { @media (--big-viewport) { .listItem { - border: 1px solid #e0e0e0; margin-bottom: 30px; &:last-child { @@ -460,3 +455,11 @@ span { position: relative; } } + +.minimal { + margin: 0; +} + +.minimalSelection { + background-color: #ecf4ff; +} diff --git a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js index 9afd10efb..5a0956161 100644 --- a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js @@ -6,7 +6,8 @@ import UserDetail from '../components/UserDetail'; import withQuery from 'coral-framework/hocs/withQuery'; import {getSlotsFragments} from 'coral-framework/helpers/plugins'; import {getDefinitionName} from 'coral-framework/utils'; -import {changeUserDetailStatuses} from 'coral-admin/src/actions/moderation'; +import {changeUserDetailStatuses, toggleSelectCommentInUserDetail} from 'coral-admin/src/actions/moderation'; +import {withSetCommentStatus} from 'coral-framework/graphql/mutations'; import Comment from './Comment'; const commentConnectionFragment = gql` @@ -31,12 +32,23 @@ class UserDetailContainer extends React.Component { hideUserDetail: PropTypes.func.isRequired } + // status can be 'ACCEPTED' or 'REJECTED' + bulkSetCommentStatus = (status) => { + this.props.moderation.userDetailSelectedIds.forEach((commentId) => { + this.props.setCommentStatus({commentId, status}); + }); + } + render () { if (!('user' in this.props.root)) { return null; } - return ; + return ; } } @@ -79,10 +91,14 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = (dispatch) => ({ - ...bindActionCreators({changeUserDetailStatuses}, dispatch) + ...bindActionCreators({ + changeUserDetailStatuses, + toggleSelectCommentInUserDetail + }, dispatch) }); export default compose( connect(mapStateToProps, mapDispatchToProps), withUserDetailQuery, + withSetCommentStatus, )(UserDetailContainer); From 49eecaa65b003a92060a854b8c01cb0b50ad2e31 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Mon, 5 Jun 2017 16:22:17 -0400 Subject: [PATCH 21/48] Fix typos --- locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index c99192ed5..03fad3bc5 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -158,8 +158,8 @@ en: edit_window_expired_close: "Close" edit_window_timer_prefix: "Edit Window: " second: "second" - secondsPlural: "seconds" - unexpectedError: "Unexpected error while saving changes. Sorry!" + seconds_plural: "seconds" + unexpected_error: "Unexpected error while saving changes. Sorry!" email: confirm: has_been_requested: "A email confirmation has been requested for the following account:" From c4a41af33b1eccb21551f4c9f5cd03cb65b71b4f Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Mon, 5 Jun 2017 14:40:03 -0600 Subject: [PATCH 22/48] clear selected ids when panel is closed --- client/coral-admin/src/reducers/moderation.js | 4 +++- .../coral-admin/src/routes/Moderation/components/Comment.js | 1 + .../coral-admin/src/routes/Moderation/components/styles.css | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 579800423..7975e3e39 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -67,7 +67,9 @@ export default function moderation (state = initialState, action) { case actions.VIEW_USER_DETAIL: return state.set('userDetailId', action.userId); case actions.HIDE_USER_DETAIL: - return state.set('userDetailId', null); + return state + .set('userDetailId', null) + .update('userDetailSelectedIds', (set) => set.clear()); case actions.CHANGE_USER_DETAIL_STATUSES: return state .set('userDetailActiveTab', action.tab) diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index edb474493..9d1019b5e 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -75,6 +75,7 @@ const Comment = ({ { minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && ( Date: Mon, 5 Jun 2017 14:00:21 -0700 Subject: [PATCH 23/48] fix lint errors --- client/coral-embed-stream/src/components/Embed.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index bb95ac290..3a50ad047 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -10,7 +10,7 @@ import ProfileContainer from 'coral-settings/containers/ProfileContainer'; import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; -export default class Embed extends React.Component { +export default class Embed extends React.Component { changeTab = (tab) => { switch (tab) { case 0: @@ -42,9 +42,9 @@ export default class Embed extends React.Component {
- - {t('framework.my_profile')} - {t('framework.configure_stream')} + + {t('framework.my_profile')} + {t('framework.configure_stream')} {commentId &&
); From 124ea19b525715ac6c626417e1646c11325ac6a3 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Tue, 6 Jun 2017 12:03:12 -0400 Subject: [PATCH 38/48] Update en.yml --- locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 03fad3bc5..90182abb7 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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." @@ -322,7 +322,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}." From 3d8478dfb24da7d017921d318ddd22addf9db363 Mon Sep 17 00:00:00 2001 From: Erica Irving Date: Tue, 6 Jun 2017 12:17:32 -0400 Subject: [PATCH 39/48] add semantic classname to active tab --- client/coral-ui/components/Tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-ui/components/Tab.js b/client/coral-ui/components/Tab.js index 9b229c4e8..8634c8c88 100644 --- a/client/coral-ui/components/Tab.js +++ b/client/coral-ui/components/Tab.js @@ -4,7 +4,7 @@ import styles from './Tab.css'; export default ({children, tabId, active, onTabClick, cStyle = 'base', ...props}) => (
  • onTabClick(tabId)} > {children} From afdcdff51e058524aa8ed728cdbda323eddc320d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 23:18:37 +0700 Subject: [PATCH 40/48] Handle ignored user case --- client/coral-embed-stream/src/components/Comment.js | 11 +++++++++-- client/coral-embed-stream/src/components/LoadMore.js | 3 +++ client/coral-embed-stream/src/containers/Stream.js | 2 +- client/coral-embed-stream/src/graphql/utils.js | 3 --- locales/en.yml | 1 + locales/es.yml | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 177ac457d..730c2ca4a 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -204,6 +204,11 @@ 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) { @@ -292,6 +297,8 @@ class Comment extends React.Component { } = 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', @@ -524,8 +531,8 @@ class Comment extends React.Component {
    view.length} + replyCount={replyCount} + moreComments={hasMoreComments} loadMore={this.loadNewReplies} />
    diff --git a/client/coral-embed-stream/src/components/LoadMore.js b/client/coral-embed-stream/src/components/LoadMore.js index 31001fcd2..ca2816d10 100644 --- a/client/coral-embed-stream/src/components/LoadMore.js +++ b/client/coral-embed-stream/src/components/LoadMore.js @@ -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'); } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 360ee2ef9..6b4ae71ed 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -159,7 +159,7 @@ const commentFragment = gql` id ...${getDefinitionName(Comment.fragments.comment)} replyCount(excludeIgnored: $excludeIgnored) - replies { + replies(excludeIgnored: $excludeIgnored) { nodes { id ...${getDefinitionName(Comment.fragments.comment)} diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index d2a557f23..eff1e2439 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -177,8 +177,5 @@ function findAndInsertFetchedComments(parent, comments, parent_id) { } export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) { - if (!comments.nodes.length) { - return root; - } return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id)); } diff --git a/locales/en.yml b/locales/en.yml index 03fad3bc5..3f3b7fc0d 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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" diff --git a/locales/es.yml b/locales/es.yml index 9e7a8bcba..0802a7604 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -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" From 6fcd003e76e495492d3cb554996f155c322cfe40 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 6 Jun 2017 10:32:14 -0600 Subject: [PATCH 41/48] remove console.log --- client/coral-embed-stream/src/components/Embed.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index bf1aa2292..145e8340a 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -38,8 +38,6 @@ export default class Embed extends React.Component { const {asset: {totalCommentCount}} = this.props.root; const {user} = this.props.auth; - console.log('activeTab', activeTab); - return (
    From c3e32f37dcdeb27703a74156f4e2ffb09b351f77 Mon Sep 17 00:00:00 2001 From: Erica Irving Date: Tue, 6 Jun 2017 13:43:45 -0400 Subject: [PATCH 42/48] add coral- prefix as requested --- client/coral-ui/components/Tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-ui/components/Tab.js b/client/coral-ui/components/Tab.js index 8634c8c88..cc2ee82d7 100644 --- a/client/coral-ui/components/Tab.js +++ b/client/coral-ui/components/Tab.js @@ -4,7 +4,7 @@ import styles from './Tab.css'; export default ({children, tabId, active, onTabClick, cStyle = 'base', ...props}) => (
  • onTabClick(tabId)} > {children} From 7cb05cebefda9a1adfadf26907961ab22d5edb0a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 7 Jun 2017 01:32:37 +0700 Subject: [PATCH 43/48] Use wss when connection is secure --- client/coral-framework/services/client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index facb23d06..6095e17ab 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -9,7 +9,8 @@ export function getClient() { return client; } - const wsClient = new SubscriptionClient(`ws://${location.host}/api/v1/live`, { + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + const wsClient = new SubscriptionClient(`${protocol}://${location.host}/api/v1/live`, { reconnect: true }); From 54655638566f7c2cbebcb89b87ab98368c47eb28 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 6 Jun 2017 12:52:26 -0600 Subject: [PATCH 44/48] reload and clear selections --- client/coral-admin/src/actions/moderation.js | 2 ++ client/coral-admin/src/constants/moderation.js | 1 + client/coral-admin/src/reducers/moderation.js | 2 ++ .../routes/Moderation/components/UserDetail.js | 18 ++++++++++++++---- .../routes/Moderation/containers/UserDetail.js | 16 +++++++++++++--- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index de57a6102..37e09590d 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -43,6 +43,8 @@ export const changeUserDetailStatuses = (tab) => { return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses}; }; +export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS}); + export const toggleSelectCommentInUserDetail = (id, active) => { return { type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT, diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index a4d75c202..374de616a 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -11,3 +11,4 @@ export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER'; export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES'; export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT'; export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT'; +export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS'; diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 7975e3e39..1b95bdb90 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -70,6 +70,8 @@ export default function moderation (state = initialState, action) { return state .set('userDetailId', null) .update('userDetailSelectedIds', (set) => set.clear()); + case actions.CLEAR_USER_DETAIL_SELECTIONS: + return state.update('userDetailSelectedIds', (set) => set.clear()); case actions.CHANGE_USER_DETAIL_STATUSES: return state .set('userDetailActiveTab', action.tab) diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.js b/client/coral-admin/src/routes/Moderation/components/UserDetail.js index 97321c1d6..76bc100fc 100644 --- a/client/coral-admin/src/routes/Moderation/components/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.js @@ -39,6 +39,18 @@ export default class UserDetail extends React.Component { } } + rejectThenReload = (info) => { + this.props.rejectComment(info).then(() => { + this.props.data.refetch(); + }); + } + + acceptThenReload = (info) => { + this.props.acceptComment(info).then(() => { + this.props.data.refetch(); + }); + } + render () { const { root: { @@ -57,8 +69,6 @@ export default class UserDetail extends React.Component { bulkSetCommentStatus, showBanUserDialog, showSuspendUserDialog, - acceptComment, - rejectComment, hideUserDetail } = this.props; const localProfile = user.profiles.find((p) => p.provider === 'local'); @@ -145,8 +155,8 @@ export default class UserDetail extends React.Component { actions={actionsMap[status]} showBanUserDialog={showBanUserDialog} showSuspendUserDialog={showSuspendUserDialog} - acceptComment={acceptComment} - rejectComment={rejectComment} + acceptComment={this.acceptThenReload} + rejectComment={this.rejectThenReload} selected={selected} toggleSelect={toggleSelect} currentAsset={null} diff --git a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js index 5a0956161..9604f27a0 100644 --- a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js @@ -6,7 +6,11 @@ import UserDetail from '../components/UserDetail'; import withQuery from 'coral-framework/hocs/withQuery'; import {getSlotsFragments} from 'coral-framework/helpers/plugins'; import {getDefinitionName} from 'coral-framework/utils'; -import {changeUserDetailStatuses, toggleSelectCommentInUserDetail} from 'coral-admin/src/actions/moderation'; +import { + changeUserDetailStatuses, + clearUserDetailSelections, + toggleSelectCommentInUserDetail +} from 'coral-admin/src/actions/moderation'; import {withSetCommentStatus} from 'coral-framework/graphql/mutations'; import Comment from './Comment'; @@ -34,8 +38,13 @@ class UserDetailContainer extends React.Component { // status can be 'ACCEPTED' or 'REJECTED' bulkSetCommentStatus = (status) => { - this.props.moderation.userDetailSelectedIds.forEach((commentId) => { - this.props.setCommentStatus({commentId, status}); + const changes = this.props.moderation.userDetailSelectedIds.map((commentId) => { + return this.props.setCommentStatus({commentId, status}); + }); + + Promise.all(changes).then(() => { + this.props.data.refetch(); // some comments may have moved out of this tab + this.props.clearUserDetailSelections(); // un-select everything }); } @@ -93,6 +102,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ ...bindActionCreators({ changeUserDetailStatuses, + clearUserDetailSelections, toggleSelectCommentInUserDetail }, dispatch) }); From 4e19d5eecf73bb26d0798ca353ed8e4b85d469d6 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 6 Jun 2017 13:11:31 -0600 Subject: [PATCH 45/48] don't have functions re-created every render --- .../Moderation/components/UserDetail.js | 30 ++++++++++--------- .../Moderation/containers/UserDetail.js | 11 ++++++- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.js b/client/coral-admin/src/routes/Moderation/components/UserDetail.js index 76bc100fc..0fb355486 100644 --- a/client/coral-admin/src/routes/Moderation/components/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.js @@ -18,7 +18,8 @@ export default class UserDetail extends React.Component { rejectComment: PropTypes.func.isRequired, changeStatus: PropTypes.func.isRequired, toggleSelect: PropTypes.func.isRequired, - bulkSetCommentStatus: PropTypes.func.isRequired, + bulkAccept: PropTypes.func.isRequired, + bulkReject: PropTypes.func.isRequired, } copyPermalink = () => { @@ -31,14 +32,6 @@ export default class UserDetail extends React.Component { } } - changeStatus = (tab) => { - if (tab === 'all') { - this.props.changeStatus('all'); - } else if (tab === 'rejected') { - this.props.changeStatus('rejected'); - } - } - rejectThenReload = (info) => { this.props.rejectComment(info).then(() => { this.props.data.refetch(); @@ -51,6 +44,14 @@ export default class UserDetail extends React.Component { }); } + showAll = () => { + this.props.changeStatus('all'); + } + + showRejected = () => { + this.props.changeStatus('rejected'); + } + render () { const { root: { @@ -66,7 +67,8 @@ export default class UserDetail extends React.Component { bannedWords, suspectWords, toggleSelect, - bulkSetCommentStatus, + bulkAccept, + bulkReject, showBanUserDialog, showSuspendUserDialog, hideUserDetail @@ -116,20 +118,20 @@ export default class UserDetail extends React.Component { selectedIds.length === 0 ? (
      -
    • All
    • -
    • Rejected
    • +
    • All
    • +
    • Rejected
    ) : (
  • + {/* Edit Comment Timeframe */} + +
    {t('configure.edit_comment_timeframe_heading')}
    +

    + {t('configure.edit_comment_timeframe_text_pre')} +   + +   + {t('configure.edit_comment_timeframe_text_post')} +

    +
    Date: Tue, 6 Jun 2017 16:49:51 -0600 Subject: [PATCH 47/48] moved plugin to lazy load --- services/passport.js | 1 + services/users.js | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/services/passport.js b/services/passport.js index 03602cdb6..d42dfa295 100644 --- a/services/passport.js +++ b/services/passport.js @@ -40,6 +40,7 @@ const SetTokenForSafari = (req, res, token) => { if (browser.ios || browser.safari) { res.cookie('authorization', token, { httpOnly: true, + secure: process.env.NODE_ENV === 'production', expires: new Date(Date.now() + ms(JWT_EXPIRY)) }); } diff --git a/services/users.js b/services/users.js index 5c6440a0e..9bca10ea5 100644 --- a/services/users.js +++ b/services/users.js @@ -930,16 +930,21 @@ module.exports = class UsersService { // Extract all the tokenUserNotFound plugins so we can integrate with other // providers. -const tokenUserNotFoundHooks = require('./plugins') - .get('server', 'tokenUserNotFound') - .map(({plugin, tokenUserNotFound}) => { - debug(`added plugin '${plugin.name}' to tokenUserNotFound hooks`); +let tokenUserNotFoundHooks = null; - return tokenUserNotFound; - }); - -// Provide a function that +// Provide a function that can loop over the hooks and search for a provider +// can crack the token to a user. const lookupUserNotFound = async (token) => { + if (!Array.isArray(tokenUserNotFoundHooks)) { + tokenUserNotFoundHooks = require('./plugins') + .get('server', 'tokenUserNotFound') + .map(({plugin, tokenUserNotFound}) => { + debug(`added plugin '${plugin.name}' to tokenUserNotFound hooks`); + + return tokenUserNotFound; + }); + } + for (let hook of tokenUserNotFoundHooks) { let user = await hook(token); if (user !== null && typeof user !== 'undefined') { From ceb99cf20588bdc450d1b2822d219eb32f690384 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 7 Jun 2017 17:45:33 +0700 Subject: [PATCH 48/48] Better refetch detection --- client/coral-embed-stream/src/components/Comment.js | 1 + client/coral-embed-stream/src/reducers/embed.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 71117563d..228dd4c41 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -297,6 +297,7 @@ class Comment extends React.Component { } = 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); diff --git a/client/coral-embed-stream/src/reducers/embed.js b/client/coral-embed-stream/src/reducers/embed.js index 8ccf4cbbc..06073c54e 100644 --- a/client/coral-embed-stream/src/reducers/embed.js +++ b/client/coral-embed-stream/src/reducers/embed.js @@ -4,6 +4,7 @@ const initialState = { activeTab: 'stream', previousTab: '', refetching: false, + refetchRequestId: 0, }; export default function stream(state = initialState, action) { @@ -18,7 +19,8 @@ export default function stream(state = initialState, action) { if (action.queryString.indexOf('query CoralEmbedStream_Embed(') >= 0) { return { ...state, - refetching: action.isRefetch, + refetching: action.isRefetch ? true : state.refetching, + refetchRequestId: action.isRefetch ? action.requestId : state.refetchRequestId, }; } return state; @@ -26,7 +28,7 @@ export default function stream(state = initialState, action) { if (action.operationName === 'CoralEmbedStream_Embed') { return { ...state, - refetching: false, + refetching: action.requestId === state.refetchRequestId ? false : state.refetching, }; } return state;