From 12d3273d3de2d1da97093e912a0780d859ab9cfb Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 4 Aug 2017 22:02:44 +0700 Subject: [PATCH 001/109] Support load more in user history --- .../coral-admin/src/components/LoadMore.css | 21 +++++++ .../Moderation => }/components/LoadMore.js | 2 +- .../coral-admin/src/components/UserDetail.js | 8 ++- .../src/components/UserDetailComment.css | 4 ++ .../coral-admin/src/containers/UserDetail.js | 56 ++++++++++++++++++- .../Moderation/components/ModerationQueue.js | 2 +- .../routes/Moderation/components/styles.css | 22 +------- graph/helpers/response.js | 9 +-- 8 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 client/coral-admin/src/components/LoadMore.css rename client/coral-admin/src/{routes/Moderation => }/components/LoadMore.js (92%) diff --git a/client/coral-admin/src/components/LoadMore.css b/client/coral-admin/src/components/LoadMore.css new file mode 100644 index 000000000..64c51f870 --- /dev/null +++ b/client/coral-admin/src/components/LoadMore.css @@ -0,0 +1,21 @@ +.loadMoreContainer { + display: flex; + justify-content: center; + width: 100%; +} + +.loadMore { + width: 100%; + text-align: center; + color: #FFF; + max-width: 660px; + margin-bottom: 30px; + background-color: #2376D8; + cursor: pointer; +} + +.loadMore:hover { + background-color: #4399FF; +} + + diff --git a/client/coral-admin/src/routes/Moderation/components/LoadMore.js b/client/coral-admin/src/components/LoadMore.js similarity index 92% rename from client/coral-admin/src/routes/Moderation/components/LoadMore.js rename to client/coral-admin/src/components/LoadMore.js index 612629647..ac7ea38d6 100644 --- a/client/coral-admin/src/routes/Moderation/components/LoadMore.js +++ b/client/coral-admin/src/components/LoadMore.js @@ -1,6 +1,6 @@ import React, {PropTypes} from 'react'; import {Button} from 'coral-ui'; -import styles from './styles.css'; +import styles from './LoadMore.css'; const LoadMore = ({loadMore, showLoadMore}) =>
diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index eab25c41d..dcc40a029 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -6,6 +6,7 @@ import {Slot} from 'coral-framework/components'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; import {actionsMap} from '../utils/moderationQueueActionsMap'; import ClickOutside from 'coral-framework/components/ClickOutside'; +import LoadMore from '../components/LoadMore'; export default class UserDetail extends React.Component { @@ -59,7 +60,7 @@ export default class UserDetail extends React.Component { user, totalComments, rejectedComments, - comments: {nodes} + comments: {nodes, hasNextPage} }, activeTab, selectedCommentIds, @@ -70,6 +71,7 @@ export default class UserDetail extends React.Component { bulkReject, hideUserDetail, viewUserDetail, + loadMore, } = this.props; const localProfile = user.profiles.find((p) => p.provider === 'local'); @@ -167,6 +169,10 @@ export default class UserDetail extends React.Component { }) }
+ ); diff --git a/client/coral-admin/src/components/UserDetailComment.css b/client/coral-admin/src/components/UserDetailComment.css index 0e10f00c0..b042aea39 100644 --- a/client/coral-admin/src/components/UserDetailComment.css +++ b/client/coral-admin/src/components/UserDetailComment.css @@ -8,6 +8,10 @@ min-height: 0; } +.root:last-child { + border: 0; +} + .rootSelected { background-color: #ecf4ff; } diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index a54e70734..e59629a85 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -14,6 +14,7 @@ import { } from 'coral-admin/src/actions/userDetail'; import {withSetCommentStatus} from 'coral-framework/graphql/mutations'; import UserDetailComment from './UserDetailComment'; +import update from 'immutability-helper'; const commentConnectionFragment = gql` fragment CoralAdmin_Moderation_CommentConnection on CommentConnection { @@ -32,6 +33,7 @@ const slots = [ ]; class UserDetailContainer extends React.Component { + isLoadingMore = false; // status can be 'ACCEPTED' or 'REJECTED' bulkSetCommentStatus = (status) => { @@ -40,7 +42,6 @@ class UserDetailContainer extends React.Component { }); Promise.all(changes).then(() => { - this.props.data.refetch(); // some comments may have moved out of this tab this.props.clearUserDetailSelections(); // un-select everything }); } @@ -61,12 +62,53 @@ class UserDetailContainer extends React.Component { return this.props.setCommentStatus({commentId, status: 'REJECTED'}); } + loadMore = () => { + if (this.isLoadingMore) { + return; + } + + this.isLoadingMore = true; + const variables = { + limit: 10, + cursor: this.props.root.comments.endCursor, + author_id: this.props.data.variables.author_id, + statuses: this.props.data.variables.statuses, + }; + this.props.data.fetchMore({ + query: LOAD_MORE_QUERY, + variables, + updateQuery: (prev, {fetchMoreResult:{comments}}) => { + return update(prev, { + comments: { + nodes: {$push: comments.nodes}, + hasNextPage: {$set: comments.hasNextPage}, + startCursor: {$set: comments.startCursor}, + endCursor: {$set: comments.endCursor}, + }, + }); + } + }) + .then(() => { + this.isLoadingMore = false; + }) + .catch((err) => { + this.isLoadingMore = false; + throw err; + }); + }; + + componentWillReceiveProps(next) { + if (this.props.userId === null && next.userId) { + next.data.refetch(); + } + } + render () { if (!this.props.userId) { return null; } - const loading = !('user' in this.props.root) || this.props.root.user.id !== this.props.userId; + const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0; return ; } } +const LOAD_MORE_QUERY = gql` + query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) { + comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) { + ...CoralAdmin_Moderation_CommentConnection + } + } + ${commentConnectionFragment} +`; + export const withUserDetailQuery = withQuery(gql` query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) { user(id: $author_id) { diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 57b551df2..7f96c7c90 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -4,7 +4,7 @@ import Comment from '../containers/Comment'; import styles from './styles.css'; import EmptyCard from '../../../components/EmptyCard'; import {actionsMap} from '../../../utils/moderationQueueActionsMap'; -import LoadMore from './LoadMore'; +import LoadMore from '../../../components/LoadMore'; import t from 'coral-framework/services/i18n'; import {CSSTransitionGroup} from 'react-transition-group'; diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index ccc762641..72c5e4d63 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -397,26 +397,6 @@ span { } } -.loadMoreContainer { - display: flex; - justify-content: center; - width: 100%; -}; - -.loadMore { - width: 100%; - text-align: center; - color: #FFF; - max-width: 660px; - margin-bottom: 30px; - background-color: #2376D8; - cursor: pointer; -} - -.loadMore:hover { - background-color: #4399FF; -} - .tabIcon { position: relative; top: 3px; @@ -499,4 +479,4 @@ span { right: 0px; top: 0px; text-align: right; -} \ No newline at end of file +} diff --git a/graph/helpers/response.js b/graph/helpers/response.js index ebdbc02ec..f5e7a54f4 100644 --- a/graph/helpers/response.js +++ b/graph/helpers/response.js @@ -2,18 +2,19 @@ const errors = require('../../errors'); const {Error: {ValidationError}} = require('mongoose'); /** - * Wraps up a promise to return an object with the resolution of the promise + * Wraps up a promise or value to return an object with the resolution of the promise * keyed at `key` or an error caught at `errors`. */ -const wrapResponse = (key) => (promise) => { - return promise.then((value) => { +const wrapResponse = (key) => (promiseOrValue) => { + return Promise.resolve(promiseOrValue).then((value) => { let res = {}; if (key) { res[key] = value; } return res; - }).catch((err) => { + }) + .catch((err) => { if (err instanceof errors.APIError) { return { errors: [err] From 5ceda4f2d27359a006b1b19a3021779768632b39 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 4 Aug 2017 22:35:26 +0700 Subject: [PATCH 002/109] Update my comments after posting --- client/coral-embed-stream/src/graphql/index.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index a16af23e7..f52cc4e7d 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -73,6 +73,11 @@ const extension = { created_at status replyCount + asset { + id + title + url + } tags { tag { name @@ -190,6 +195,15 @@ const extension = { } return insertCommentIntoEmbedQuery(prev, comment); }, + CoralEmbedStream_Profile: (prev, {mutationResult: {data: {createComment: {comment}}}}) => { + return update(prev, { + me: { + comments: { + nodes: {$unshift: [comment]}, + }, + }, + }); + }, } }), EditComment: () => ({ From 0df4fd1e378527aa700a0f60bf10d274bf6dff35 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 4 Aug 2017 23:24:40 +0700 Subject: [PATCH 003/109] Adjust styling --- client/coral-admin/src/components/LoadMore.js | 5 +++-- client/coral-admin/src/components/UserDetail.css | 9 +++++++++ client/coral-admin/src/components/UserDetail.js | 1 + client/coral-ui/components/Drawer.css | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/client/coral-admin/src/components/LoadMore.js b/client/coral-admin/src/components/LoadMore.js index ac7ea38d6..969c6734f 100644 --- a/client/coral-admin/src/components/LoadMore.js +++ b/client/coral-admin/src/components/LoadMore.js @@ -1,9 +1,10 @@ import React, {PropTypes} from 'react'; import {Button} from 'coral-ui'; import styles from './LoadMore.css'; +import cn from 'classnames'; -const LoadMore = ({loadMore, showLoadMore}) => -
+const LoadMore = ({loadMore, showLoadMore, className, ...rest}) => +
{ showLoadMore &&
diff --git a/client/coral-ui/components/Drawer.css b/client/coral-ui/components/Drawer.css index d6a7e6871..88c30425f 100644 --- a/client/coral-ui/components/Drawer.css +++ b/client/coral-ui/components/Drawer.css @@ -3,7 +3,7 @@ min-width: 550px; position: fixed; top: 0; - right: -17px; + right: 0px; bottom: 0; background-color: white; transition: transform 500ms ease-in-out; From 76b125f042dd4da55c997b10c206712a8072055b Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Fri, 4 Aug 2017 13:42:19 -0400 Subject: [PATCH 004/109] adjustments to comment queue counters --- .../src/routes/Moderation/components/CommentCount.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/CommentCount.css b/client/coral-admin/src/routes/Moderation/components/CommentCount.css index 4d24d5702..998133d07 100644 --- a/client/coral-admin/src/routes/Moderation/components/CommentCount.css +++ b/client/coral-admin/src/routes/Moderation/components/CommentCount.css @@ -1,16 +1,16 @@ .count { display: inline-block; - background: #989797; + background: #616161; margin: 2px; vertical-align: middle; - padding: 1px 7px; + padding: 1px 5px; border-radius: 2px; margin-left: 2px; - line-height: 20px; + line-height: 18px; box-sizing: border-box; - height: 21px; + height: 18px; right: 0; - margin-top: -2px; + margin-top: 0px; font-size: 12px; color: white; } From 11b2bc265a0e1469383b0613a0a4b3066bf32820 Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Fri, 4 Aug 2017 13:50:14 -0400 Subject: [PATCH 005/109] comment tag/marker updates for size --- client/coral-admin/src/components/CommentType.css | 5 +++-- .../client/components/ModTag.css | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/components/CommentType.css b/client/coral-admin/src/components/CommentType.css index b5dbc229b..61a65f89a 100644 --- a/client/coral-admin/src/components/CommentType.css +++ b/client/coral-admin/src/components/CommentType.css @@ -3,10 +3,11 @@ color: white; background: grey; box-sizing: border-box; - padding: 2px 8px; + padding: 0px 5px; border-radius: 2px; font-size: 12px; - height: 28px; + height: 24px; + letter-spacing: 0.4px; > i { font-size: 14px; diff --git a/plugins/talk-plugin-featured-comments/client/components/ModTag.css b/plugins/talk-plugin-featured-comments/client/components/ModTag.css index c8ba1b6ba..683c16570 100644 --- a/plugins/talk-plugin-featured-comments/client/components/ModTag.css +++ b/plugins/talk-plugin-featured-comments/client/components/ModTag.css @@ -4,13 +4,14 @@ color: #696969; background-color: white; box-sizing: border-box; - padding: 2px 8px; + padding: 0px 5px; border-radius: 2px; font-size: 12px; - height: 28px; + height: 24px; transition: background-color .2s cubic-bezier(.4,0,.2,1), color .2s cubic-bezier(.4,0,.2,1), border-color .2s cubic-bezier(.4,0,.2,1); margin: 2px 0px; letter-spacing: 0.4px; + } .tag:hover { @@ -39,4 +40,3 @@ font-size: 15px; vertical-align: text-bottom; } - \ No newline at end of file From 567f8bf94eba5f02db08c83a0e269f4e5d9b0b5f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 7 Aug 2017 14:54:11 +1000 Subject: [PATCH 006/109] adjusted wrapResponse function --- graph/helpers/response.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/graph/helpers/response.js b/graph/helpers/response.js index f5e7a54f4..1db1e9b89 100644 --- a/graph/helpers/response.js +++ b/graph/helpers/response.js @@ -6,15 +6,17 @@ const {Error: {ValidationError}} = require('mongoose'); * keyed at `key` or an error caught at `errors`. */ -const wrapResponse = (key) => (promiseOrValue) => { - return Promise.resolve(promiseOrValue).then((value) => { +const wrapResponse = (key) => async (promise) => { + try { + let value = await promise; + let res = {}; if (key) { res[key] = value; } + return res; - }) - .catch((err) => { + } catch (err) { if (err instanceof errors.APIError) { return { errors: [err] @@ -26,7 +28,7 @@ const wrapResponse = (key) => (promiseOrValue) => { } throw err; - }); + } }; module.exports = wrapResponse; From ddadd6c5d32bd17c1741e95c8e01e4f54a3c9d68 Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Mon, 7 Aug 2017 10:26:42 -0500 Subject: [PATCH 007/109] adjustments/fixes to approved rejected widths --- client/coral-admin/src/components/CommentType.css | 3 ++- client/coral-admin/src/components/ModerationList.css | 1 - client/coral-ui/components/Button.css | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/components/CommentType.css b/client/coral-admin/src/components/CommentType.css index 61a65f89a..e4851275e 100644 --- a/client/coral-admin/src/components/CommentType.css +++ b/client/coral-admin/src/components/CommentType.css @@ -8,7 +8,8 @@ font-size: 12px; height: 24px; letter-spacing: 0.4px; - + margin-bottom: 1px; + > i { font-size: 14px; vertical-align: text-top; diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css index 89e43e02a..4ac894290 100644 --- a/client/coral-admin/src/components/ModerationList.css +++ b/client/coral-admin/src/components/ModerationList.css @@ -186,7 +186,6 @@ .actionButton { transform: scale(.8); margin: 0; - width: 140px; } .minimal { diff --git a/client/coral-ui/components/Button.css b/client/coral-ui/components/Button.css index 7bd1d4379..1b7efb205 100644 --- a/client/coral-ui/components/Button.css +++ b/client/coral-ui/components/Button.css @@ -27,9 +27,10 @@ } .icon { - margin-right: 13px; + margin-right: 5px; font-size: 18px; vertical-align: middle; + margin-top: -3px; } .type--black { @@ -143,7 +144,7 @@ border-radius: 3px; text-transform: capitalize; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); - width: 128px; + width: 129px; &:hover { box-shadow: none; @@ -166,7 +167,7 @@ border-radius: 3px; text-transform: capitalize; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); - width: 128px; + width: 129px; &:hover { color: white; From 27216cbfdbc7c8208516c3cc720a2df8e9cf66da Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Mon, 7 Aug 2017 10:32:21 -0500 Subject: [PATCH 008/109] hierarchy for asset search title --- .../coral-admin/src/routes/Moderation/components/styles.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index ccc762641..b821d9429 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -111,7 +111,7 @@ span { color: white; text-transform: capitalize; font-weight: 400; - font-size: 15px; + font-size: 20px; letter-spacing: 1px; transition: background-color 200ms; opacity: 1; @@ -490,7 +490,7 @@ span { .searchTrigger { position: relative; - top: .3em; + top: .2em; } .adminCommentInfoBar { @@ -499,4 +499,4 @@ span { right: 0px; top: 0px; text-align: right; -} \ No newline at end of file +} From 53cac48e3b365bc9e4066b5b8e9163ecd7307e75 Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Mon, 7 Aug 2017 11:53:39 -0500 Subject: [PATCH 009/109] resizing of the comment component --- client/coral-admin/src/routes/Moderation/components/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index b821d9429..cccf85f78 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -173,7 +173,7 @@ span { border-bottom: 1px solid #e0e0e0; font-size: 18px; width: 100%; - max-width: 700px; + max-width: 650px; min-width: 400px; margin: 0 auto; position: relative; From a22d0f7536a67219b654ed8d385f1e692e943151 Mon Sep 17 00:00:00 2001 From: Erik Reyna Date: Mon, 7 Aug 2017 14:00:53 -0400 Subject: [PATCH 010/109] expose talk-stream-comment-container class --- 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 34f672d8a..ed30e2e32 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -406,7 +406,7 @@ export default class Comment extends React.Component { inline /> -
+
From 1e8d2ce3a665f5cd065e991dc3a537f015ebb83a Mon Sep 17 00:00:00 2001 From: Erik Reyna Date: Mon, 7 Aug 2017 14:11:51 -0400 Subject: [PATCH 011/109] expose talk-stream-comment-avatar class --- 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 34f672d8a..1c57e2d1e 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -400,7 +400,7 @@ export default class Comment extends React.Component {
Date: Tue, 8 Aug 2017 15:37:45 +1000 Subject: [PATCH 012/109] Added whitelist question to cli-setup --- bin/cli-setup | 37 ++++++++++++++++---- package.json | 2 +- yarn.lock | 96 ++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/bin/cli-setup b/bin/cli-setup index 9ecdecc95..f95fca8d4 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -94,7 +94,7 @@ const performSetup = async () => { name: 'requireEmailConfirmation', default: settings.requireEmailConfirmation, message: 'Should emails always be confirmed' - } + }, ]); // Update the settings that were changed. @@ -104,6 +104,31 @@ const performSetup = async () => { } }); + answers = await inquirer.prompt([ + { + type: 'confirm', + name: 'inputWhitelistedDomains', + default: true, + message: 'Would you like to specify a whitelisted domain' + }, + { + type: 'input', + name: 'whitelistedDomain', + message: 'Whitelisted Domain', + validate: (input) => { + if (input && input.length > 0) { + return true; + } + + return 'Whitelisted Domain cannot be empty.'; + } + } + ]); + + if (answers.inputWhitelistedDomains) { + settings.domains.whitelist = [answers.whitelistedDomain]; + } + console.log('\nWe\'ll ask you some questions about your first admin user.\n'); let user = await inquirer.prompt([ @@ -147,7 +172,11 @@ const performSetup = async () => { name: 'confirmPassword', message: 'Confirm Password', type: 'password', - filter: (confirmPassword) => { + filter: (confirmPassword, {password}) => { + if (password !== confirmPassword) { + return Promise.reject(new Error('Passwords do not match')); + } + return UsersService .isValidPassword(confirmPassword) .catch((err) => { @@ -157,10 +186,6 @@ const performSetup = async () => { }, ]); - if (user.password !== user.confirmPassword) { - return Promise.reject(new Error('Passwords do not match')); - } - let {user: newUser} = await SetupService.setup({ settings: settings.toObject(), user: { diff --git a/package.json b/package.json index 476e7eda5..8e83f17a9 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "graphql-tools": "^0.10.1", "helmet": "^3.5.0", "immutability-helper": "^2.2.0", - "inquirer": "^3.0.6", + "inquirer": "^3.2.1", "joi": "^10.4.1", "jsonwebtoken": "^7.3.0", "jwt-decode": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 255dc7415..a5d73a756 100644 --- a/yarn.lock +++ b/yarn.lock @@ -162,6 +162,10 @@ ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" +ansi-escapes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" + ansi-regex@^1.0.0, ansi-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-1.1.1.tgz#41c847194646375e6a1a5d10c3ca054ef9fc980d" @@ -170,10 +174,20 @@ ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + any-promise@^0.1.0, any-promise@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" @@ -1498,6 +1512,14 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + change-emitter@^0.1.2: version "0.1.6" resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" @@ -1727,7 +1749,7 @@ codemirror@*: version "5.25.2" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.25.2.tgz#8c77677ca9c9248d757d3a07ed1e89a8404850b7" -color-convert@^1.3.0: +color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -3053,10 +3075,12 @@ extend@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/extend/-/extend-1.3.0.tgz#d1516fb0ff5624d2ebf9123ea1dac5a1994004f8" -external-editor@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.1.tgz#4c597c6c88fa6410e41dbbaa7b1be2336aa31095" +external-editor@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" dependencies: + iconv-lite "^0.4.17" + jschardet "^1.4.2" tmp "^0.0.31" extglob@^0.3.1: @@ -3757,6 +3781,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3972,6 +4000,10 @@ iconv-lite@0.4.15, iconv-lite@^0.4.5, iconv-lite@~0.4.13: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" +iconv-lite@^0.4.17: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + icss-replace-symbols@1.0.2, icss-replace-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5" @@ -4103,22 +4135,23 @@ inquirer@0.8.2: rx "^2.4.3" through "^2.3.6" -inquirer@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.0.6.tgz#e04aaa9d05b7a3cb9b0f407d04375f0447190347" +inquirer@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175" dependencies: - ansi-escapes "^1.1.0" - chalk "^1.0.0" + ansi-escapes "^2.0.0" + chalk "^2.0.0" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^2.0.1" + external-editor "^2.0.4" figures "^2.0.0" lodash "^4.3.0" mute-stream "0.0.7" run-async "^2.2.0" - rx "^4.1.0" - string-width "^2.0.0" - strip-ansi "^3.0.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" through "^2.3.6" interpret@^1.0.0: @@ -4560,6 +4593,10 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" +jschardet@^1.4.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" + jsdom@^7.0.2: version "7.2.2" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-7.2.2.tgz#40b402770c2bda23469096bee91ab675e3b1fc6e" @@ -7434,6 +7471,16 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" @@ -7442,10 +7489,6 @@ rx@^2.4.3: version "2.5.3" resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566" -rx@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" - safe-buffer@^5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" @@ -7833,6 +7876,13 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string.prototype.codepointat@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" @@ -7871,6 +7921,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -7961,6 +8017,12 @@ supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" +supports-color@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" + dependencies: + has-flag "^2.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" From d1fc5668fb1ee8818017409554d0cf8b15f20743 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 8 Aug 2017 19:50:59 +0700 Subject: [PATCH 013/109] Pass config to plugins --- client/coral-framework/helpers/plugins.js | 4 ++-- plugin-api/beta/client/hocs/withReaction.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 1af2a9a59..5e5a8c0c0 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -10,7 +10,7 @@ import camelize from './camelize'; import plugins from 'pluginsConfig'; export function getSlotComponents(slot, reduxState, props = {}) { - const pluginConfig = reduxState.config.pluginConfig || {}; + const pluginConfig = reduxState.config.plugin_config || {}; return flatten(plugins // Filter out components that have slots and have been disabled in `plugin_config` @@ -39,7 +39,7 @@ export function isSlotEmpty(slot, reduxState, props) { * Returns React Elements for given slot. */ export function getSlotElements(slot, reduxState, props = {}) { - const pluginConfig = reduxState.config.pluginConfig || {}; + const pluginConfig = reduxState.config.plugin_config || {}; return getSlotComponents(slot, reduxState, props) .map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig})); } diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 40ee6e14c..7f43cc5b2 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -271,6 +271,7 @@ export default (reaction) => (WrappedComponent) => { alreadyReacted={alreadyReacted} postReaction={this.postReaction} deleteReaction={this.deleteReaction} + config={this.props.config} />; } } From c4a91d225e57657cdf56d4861373413dea374348 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 8 Aug 2017 19:57:23 +0700 Subject: [PATCH 014/109] Pass config also to withTags --- plugin-api/beta/client/hocs/withTags.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index 75d434bef..e9b5bdff0 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -68,16 +68,17 @@ export default (tag) => (WrappedComponent) => { } render() { - const {comment} = this.props; + const {comment, user, config} = this.props; const alreadyTagged = isTagged(comment.tags, TAG); return ; } } From e16dd14e47630354601175a4cedcb0c7fd84528d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 8 Aug 2017 23:33:07 +1000 Subject: [PATCH 015/109] fix bug with impl --- bin/cli-setup | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/cli-setup b/bin/cli-setup index f95fca8d4..2e5c1e5b8 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -115,6 +115,7 @@ const performSetup = async () => { type: 'input', name: 'whitelistedDomain', message: 'Whitelisted Domain', + when: ({inputWhitelistedDomains}) => inputWhitelistedDomains, validate: (input) => { if (input && input.length > 0) { return true; From 7bdd28cb9c192ef8aa4e3a83e430f32aaf237d6f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 8 Aug 2017 20:47:21 +0700 Subject: [PATCH 016/109] Emit `ui.AllCommentsPane.viewNewComments` and `ui.Comment.showMoreReplies` --- client/coral-embed-stream/src/components/AllCommentsPane.js | 3 +++ client/coral-embed-stream/src/components/Comment.js | 1 + client/coral-embed-stream/src/components/Stream.js | 1 + client/coral-embed-stream/src/containers/Stream.js | 3 ++- plugin-api/beta/client/hocs/index.js | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index 7166aa6e9..6c5d6bb23 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -93,6 +93,7 @@ class AllCommentsPane extends React.Component { viewNewComments = () => { this.setState(resetCursors); + this.props.emit('ui.AllCommentsPane.viewNewComments'); }; // getVisibileComments returns a list containing comments @@ -142,6 +143,7 @@ class AllCommentsPane extends React.Component { charCountEnable, maxCharCount, editComment, + emit, } = this.props; const {loadingState} = this.state; @@ -181,6 +183,7 @@ class AllCommentsPane extends React.Component { charCountEnable={charCountEnable} maxCharCount={maxCharCount} editComment={editComment} + emit={emit} />; })} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 34f672d8a..a8bba1ca3 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -224,6 +224,7 @@ export default class Comment extends React.Component { return; } this.setState(resetCursors); + this.props.emit('ui.Comment.showMoreReplies'); }; showReplyBox = () => { diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 1e0ff7ea9..7b9e5f566 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -292,6 +292,7 @@ class Stream extends React.Component { charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} + emit={this.props.emit} /> diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index fcedbcb0b..73676713c 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -14,7 +14,7 @@ import {editName} from 'coral-framework/actions/user'; import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream'; import Stream from '../components/Stream'; import Comment from './Comment'; -import {withFragments} from 'coral-framework/hocs'; +import {withFragments, withEmit} from 'coral-framework/hocs'; import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {Spinner} from 'coral-ui'; import { @@ -326,6 +326,7 @@ const mapDispatchToProps = (dispatch) => export default compose( withFragments(fragments), + withEmit, connect(mapStateToProps, mapDispatchToProps), withPostComment, withPostFlag, diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 68547692d..60b118522 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -3,3 +3,4 @@ export {default as withTags} from './withTags'; export {default as withFragments} from 'coral-framework/hocs/withFragments'; export {default as excludeIf} from 'coral-framework/hocs/excludeIf'; export {default as connect} from 'coral-framework/hocs/connect'; +export {default as withEmit} from 'coral-framework/hocs/withEmit'; From 976bd1e9add953de3599a1dc0afab5adf7336761 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 8 Aug 2017 22:39:24 +0700 Subject: [PATCH 017/109] Paginate my comments in profile tab --- .../containers/ProfileContainer.js | 94 +++++++++++++------ client/talk-plugin-history/CommentHistory.js | 57 ++++++++--- client/talk-plugin-history/LoadMore.js | 30 ++++++ graph/resolvers/user.js | 4 +- .../client/components/TabPane.js | 2 +- 5 files changed, 138 insertions(+), 49 deletions(-) create mode 100644 client/talk-plugin-history/LoadMore.js diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 91de923b2..0090847f7 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -1,7 +1,8 @@ import {connect} from 'react-redux'; -import {compose, graphql, gql} from 'react-apollo'; +import {compose, gql} from 'react-apollo'; import React, {Component} from 'react'; import {bindActionCreators} from 'redux'; +import {withQuery} from 'coral-framework/hocs'; import {withStopIgnoringUser} from 'coral-framework/graphql/mutations'; @@ -11,18 +12,12 @@ import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'talk-plugin-history/CommentHistory'; import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth'; +import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; +import update from 'immutability-helper'; import t from 'coral-framework/services/i18n'; class ProfileContainer extends Component { - constructor() { - super(); - - this.state = { - activeTab: 0 - }; - } - componentWillReceiveProps(nextProps) { if (!this.props.auth.loggedIn && nextProps.auth.loggedIn) { @@ -31,21 +26,40 @@ class ProfileContainer extends Component { } } - handleTabChange = (tab) => { - this.setState({ - activeTab: tab + loadMore = () => { + return this.props.data.fetchMore({ + query: LOAD_MORE_QUERY, + variables: { + limit: 5, + cursor: this.props.root.me.comments.endCursor, + }, + updateQuery: (previous, {fetchMoreResult:{comments}}) => { + const updated = update(previous, { + me: { + comments: { + nodes: { + $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'), + }, + hasNextPage: {$set: comments.hasNextPage}, + endCursor: {$set: comments.endCursor}, + }, + } + }); + return updated; + }, }); }; render() { - const {auth, asset, data, showSignInDialog, stopIgnoringUser} = this.props; - const {me} = this.props.data; + const {auth, asset, showSignInDialog, stopIgnoringUser} = this.props; + const {me} = this.props.root; + const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0; if (!auth.loggedIn) { return ; } - if (!me || data.loading) { + if (loading) { return ; } @@ -73,14 +87,40 @@ class ProfileContainer extends Component {

{t('framework.my_comments')}

{me.comments.nodes.length - ? + ? :

{t('user_no_comment')}

}
); } } -const withQuery = graphql( +const CommentFragment = gql` + fragment TalkSettings_CommentConnectionFragment on CommentConnection { + nodes { + id + body + asset { + id + title + url + } + created_at + } + endCursor + hasNextPage + } +`; + +const LOAD_MORE_QUERY = gql` + query TalkSettings_LoadMoreComments($limit: Int, $cursor: Date) { + comments(query: {limit: $limit, cursor: $cursor}) { + ...TalkSettings_CommentConnectionFragment + } + } + ${CommentFragment} +`; + +const withProfileQuery = withQuery( gql` query CoralEmbedStream_Profile { me { @@ -89,21 +129,13 @@ const withQuery = graphql( id, username, } - comments { - nodes { - id - body - asset { - id - title - url - } - created_at - } + comments(query: {limit: 10}) { + ...TalkSettings_CommentConnectionFragment } } - }` -); + } + ${CommentFragment} +`); const mapStateToProps = (state) => ({ user: state.user.toJS(), @@ -117,5 +149,5 @@ const mapDispatchToProps = (dispatch) => export default compose( connect(mapStateToProps, mapDispatchToProps), withStopIgnoringUser, - withQuery + withProfileQuery )(ProfileContainer); diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js index 72c4de982..d584d6a0a 100644 --- a/client/talk-plugin-history/CommentHistory.js +++ b/client/talk-plugin-history/CommentHistory.js @@ -1,25 +1,52 @@ import React, {PropTypes} from 'react'; import Comment from './Comment'; import styles from './CommentHistory.css'; +import LoadMore from './LoadMore'; +import {forEachError} from 'plugin-api/beta/client/utils'; -const CommentHistory = (props) => { - return ( -
-
- {props.comments.map((comment, i) => { - return ; - })} +class CommentHistory extends React.Component { + state = { + loadingState: '', + }; + + loadMore = () => { + this.setState({loadingState: 'loading'}); + this.props.loadMore() + .then(() => { + this.setState({loadingState: 'success'}); + }) + .catch((error) => { + this.setState({loadingState: 'error'}); + forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + }); + } + + render() { + const {link, comments} = this.props; + return ( +
+
+ {comments.nodes.map((comment, i) => { + return ; + })} +
+ {comments.hasNextPage && + + }
-
- ); -}; + ); + } +} CommentHistory.propTypes = { - comments: PropTypes.array.isRequired + comments: PropTypes.object.isRequired }; export default CommentHistory; diff --git a/client/talk-plugin-history/LoadMore.js b/client/talk-plugin-history/LoadMore.js new file mode 100644 index 000000000..a168ed43a --- /dev/null +++ b/client/talk-plugin-history/LoadMore.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Button} from 'coral-ui'; +import t from 'coral-framework/services/i18n'; +import cn from 'classnames'; + +class LoadMore extends React.Component { + render () { + const {loadingState, loadMore} = this.props; + const disabled = loadingState === 'loading'; + return ( +
+ +
+ ); + } +} + +LoadMore.propTypes = { + loadMore: PropTypes.func.isRequired, + loadingState: PropTypes.oneOf(['', 'loading', 'success', 'error']), +}; + +export default LoadMore; diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 2878d6561..5a851233a 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -29,12 +29,12 @@ const User = { return null; }, - comments({id}, _, {loaders: {Comments}, user}) { + comments({id}, {query}, {loaders: {Comments}, user}) { // If the user is not an admin, only return comment list for the owner of // the comments. if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === id)) { - return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); + return Comments.getByQuery(Object.assign({}, query, {author_id: id})); } return null; diff --git a/plugins/talk-plugin-featured-comments/client/components/TabPane.js b/plugins/talk-plugin-featured-comments/client/components/TabPane.js index 3cf8b2cfd..2f5d0c7c2 100644 --- a/plugins/talk-plugin-featured-comments/client/components/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/components/TabPane.js @@ -36,7 +36,7 @@ class TabPane extends React.Component { {featuredComments.hasNextPage && }
From 6913fe05ae48d5e9d399b2b8ff854547381bde33 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 9 Aug 2017 09:38:09 +1000 Subject: [PATCH 018/109] fixes #841 --- docs/_docs/05-01-development-tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/05-01-development-tools.md b/docs/_docs/05-01-development-tools.md index 609d3ef48..fc19a077c 100644 --- a/docs/_docs/05-01-development-tools.md +++ b/docs/_docs/05-01-development-tools.md @@ -1,6 +1,6 @@ --- title: Development Tooling -permalink: /docs/development/tools +permalink: /docs/development/tools/ --- ## Debugging From c62e7328c5801466315a5c632f90566b5ed8a2fa Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Wed, 9 Aug 2017 10:15:52 -0500 Subject: [PATCH 019/109] adjustments to top navigation and logo --- client/coral-admin/src/components/FlagBox.css | 2 +- client/coral-admin/src/components/ui/Logo.css | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/coral-admin/src/components/FlagBox.css b/client/coral-admin/src/components/FlagBox.css index 402a88045..232803203 100644 --- a/client/coral-admin/src/components/FlagBox.css +++ b/client/coral-admin/src/components/FlagBox.css @@ -1,6 +1,6 @@ .flagBox { border-top: 1px solid rgba(66, 66, 66, 0.12); - + margin-top: 10px; .container { padding: 0 14px; } diff --git a/client/coral-admin/src/components/ui/Logo.css b/client/coral-admin/src/components/ui/Logo.css index af2758bcb..62a223683 100644 --- a/client/coral-admin/src/components/ui/Logo.css +++ b/client/coral-admin/src/components/ui/Logo.css @@ -10,16 +10,18 @@ .logo span { display: inline-block; margin-left: 10px; - font-size: 18px; + font-size: 26px; vertical-align: middle; font-weight: 500; + color: white; } .logo { - background: #E5E5E5; + background: #696969; height: 100%; width: 128px; z-index: 10; + border-right: 1px #757575 solid; } .base { From ca422f0109ec25508efb4d9b64975dfef25dd1b7 Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Wed, 9 Aug 2017 10:23:03 -0500 Subject: [PATCH 020/109] mod queue nav styling adjust --- .../coral-admin/src/routes/Moderation/components/styles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index cccf85f78..d1d1ae697 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -18,7 +18,7 @@ .tab { flex: 1; - color: #C0C0C0; + color: #BDBDBD; text-transform: capitalize; font-weight: 100; font-size: 14px; @@ -29,7 +29,7 @@ margin-right: 20px; &:hover { color: white; - border-bottom: solid 2px #F36451; + /*border-bottom: solid 2px #F36451;*/ box-sizing: border-box; } } From b9243938bdfab47376caf9061fc8508188a70ef0 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 10 Aug 2017 10:33:45 +1000 Subject: [PATCH 021/109] Added support for changing the singing cookie name --- config.js | 19 +++++++++++++++++++ docs/_docs/02-01-configuration.md | 12 ++++++++++-- services/passport.js | 13 +++++++------ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/config.js b/config.js index 4472af26e..4a3609dc8 100644 --- a/config.js +++ b/config.js @@ -7,6 +7,8 @@ // entrypoint for the entire applications configuration. require('env-rewrite').rewrite(); +const uniq = require('lodash/uniq'); + //============================================================================== // CONFIG INITIALIZATION //============================================================================== @@ -31,6 +33,13 @@ const CONFIG = { // token. JWT_COOKIE_NAME: process.env.TALK_JWT_COOKIE_NAME || 'authorization', + // JWT_SIGNING_COOKIE_NAME will be the cookie set when cookies are issued. + // This defaults to the TALK_JWT_COOKIE_NAME value. + JWT_SIGNING_COOKIE_NAME: process.env.TALK_JWT_SIGNING_COOKIE_NAME || process.env.TALK_JWT_COOKIE_NAME || 'authorization', + + // JWT_COOKIE_NAMES declares the many cookie names used for verification. + JWT_COOKIE_NAMES: process.env.TALK_JWT_COOKIE_NAMES || null, + // JWT_CLEAR_COOKIE_LOGOUT specifies whether the named cookie should be // cleared when the user is logged out. JWT_CLEAR_COOKIE_LOGOUT: process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT ? process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT !== 'FALSE' : true, @@ -165,6 +174,16 @@ if (CONFIG.JWT_DISABLE_ISSUER) { CONFIG.JWT_ISSUER = undefined; } +// Parse and handle cookie names. +if (CONFIG.JWT_COOKIE_NAMES) { + CONFIG.JWT_COOKIE_NAMES = CONFIG.JWT_COOKIE_NAMES.split(','); +} else { + CONFIG.JWT_COOKIE_NAMES = []; +} + +// Add in the default cookie names and strip duplicates. +CONFIG.JWT_COOKIE_NAMES = uniq(CONFIG.JWT_COOKIE_NAMES.concat([CONFIG.JWT_COOKIE_NAME, CONFIG.JWT_SIGNING_COOKIE_NAME])); + //------------------------------------------------------------------------------ // External database url's //------------------------------------------------------------------------------ diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index 74bd8972e..b02376c24 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -87,8 +87,16 @@ on the contents of those variables.** These are advanced settings for fine tuning the auth integration, and is not needed in most situations. -- `TALK_JWT_COOKIE_NAME` (_optional_) - the name of the cookie to extract the - JWT from (Default `authorization`) +- `TALK_JWT_COOKIE_NAME` (_optional_) - the default cookie name to check for a + valid JWT token to use for verifying a user. (Default `authorization`) +- `TALK_JWT_SIGNING_COOKIE_NAME` (_optional_) - the default cookie name that is + use to set a cookie containing a JWT that was issued by Talk. + (Default `process.env.TALK_JWT_COOKIE_NAME`) +- `TALK_JWT_COOKIE_NAMES` (_optional_) - the different cookie names to check for + a JWT token in, seperated by `,`. By default, we always use the + `process.env.TALK_JWT_COOKIE_NAME` and `process.env.TALK_JWT_SIGNING_COOKIE_NAME` + for this value. Any additional cookie names specified here will be appended to + the list of cookie names to inspect. - `TALK_JWT_CLEAR_COOKIE_LOGOUT` (_optional_) - when `FALSE`, Talk will not clear the cookie with name `TALK_JWT_COOKIE_NAME` when logging out (Default `TRUE`) diff --git a/services/passport.js b/services/passport.js index 409bb2279..31a8ecba1 100644 --- a/services/passport.js +++ b/services/passport.js @@ -23,7 +23,8 @@ const { JWT_ALG, RECAPTCHA_SECRET, RECAPTCHA_ENABLED, - JWT_COOKIE_NAME, + JWT_SIGNING_COOKIE_NAME, + JWT_COOKIE_NAMES, JWT_CLEAR_COOKIE_LOGOUT, JWT_USER_ID_CLAIM, } = require('../config'); @@ -53,7 +54,7 @@ const GenerateToken = (user) => { const SetTokenForSafari = (req, res, token) => { const browser = bowser._detect(req.headers['user-agent']); if (browser.ios || browser.safari) { - res.cookie(JWT_COOKIE_NAME, token, { + res.cookie(JWT_SIGNING_COOKIE_NAME, token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', expires: new Date(Date.now() + ms(JWT_EXPIRY)) @@ -169,7 +170,7 @@ const HandleLogout = (req, res, next) => { // Only clear the cookie on logout if enabled. if (JWT_CLEAR_COOKIE_LOGOUT) { - res.clearCookie(JWT_COOKIE_NAME); + res.clearCookie(JWT_SIGNING_COOKIE_NAME); } res.status(204).end(); @@ -209,11 +210,11 @@ const CheckBlacklisted = async (jwt) => { const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; -let cookieExtractor = function(req) { +let cookieExtractor = (cookieName) => (req) => { let token = null; if (req && req.cookies) { - token = req.cookies[JWT_COOKIE_NAME]; + token = req.cookies[cookieName]; } return token; @@ -237,7 +238,7 @@ passport.use(new JwtStrategy({ // Prepare the extractor from the header. jwtFromRequest: ExtractJwt.fromExtractors([ - cookieExtractor, + ...JWT_COOKIE_NAMES.map(cookieExtractor), ExtractJwt.fromUrlQueryParameter('access_token'), ExtractJwt.fromAuthHeaderWithScheme('Bearer') ]), From 6d70a7b20e048166578b5313530cf0ed91719208 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 10 Aug 2017 10:46:11 +1000 Subject: [PATCH 022/109] improved code around cookie handling, added to docs --- docs/_docs/02-01-configuration.md | 8 ++++++++ services/passport.js | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index b02376c24..18a483723 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -123,6 +123,14 @@ will be used: } ``` +When our passport middleware checks for JWT tokens, it searches in the following +order: + +1. Custom cookies named from the list in `TALK_JWT_COOKIE_NAMES`. +2. Default cookies named `TALK_JWT_COOKIE_NAME` then `TALK_JWT_SIGNING_COOKIE_NAME`. +3. Query parameter `?access_token={TOKEN}`. +4. Header: `Authorization: Bearer {TOKEN}`. + ### Email - `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from diff --git a/services/passport.js b/services/passport.js index 31a8ecba1..14a2134ce 100644 --- a/services/passport.js +++ b/services/passport.js @@ -210,14 +210,20 @@ const CheckBlacklisted = async (jwt) => { const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; -let cookieExtractor = (cookieName) => (req) => { - let token = null; - +let cookieExtractor = (req) => { if (req && req.cookies) { - token = req.cookies[cookieName]; + + // Walk over all the cookie names in JWT_COOKIE_NAMES. + for (const cookieName of JWT_COOKIE_NAMES) { + + // Check to see if that cookie is set. + if (cookieName in req.cookies && req.cookies[cookieName] !== null && req.cookies[cookieName].length > 0) { + return req.cookies[cookieName]; + } + } } - return token; + return null; }; // Override the JwtVerifier method on the JwtStrategy so we can pack the @@ -238,7 +244,7 @@ passport.use(new JwtStrategy({ // Prepare the extractor from the header. jwtFromRequest: ExtractJwt.fromExtractors([ - ...JWT_COOKIE_NAMES.map(cookieExtractor), + cookieExtractor, ExtractJwt.fromUrlQueryParameter('access_token'), ExtractJwt.fromAuthHeaderWithScheme('Bearer') ]), From 0013c366860981a076a4ee3f08405456fc8f317a Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 10 Aug 2017 11:20:45 +0100 Subject: [PATCH 023/109] Update version number to 3.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 476e7eda5..087044a4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "3.0.0", + "version": "3.1.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { From 83ded4bda960420ecb9ea676e547f44addf4951e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 10 Aug 2017 18:37:14 +0700 Subject: [PATCH 024/109] Refactor into queueConfig --- client/coral-admin/src/AppRouter.js | 24 +- .../Moderation/components/Moderation.js | 41 +--- .../Moderation/components/ModerationMenu.js | 67 +----- .../Moderation/containers/Moderation.js | 226 ++++++------------ .../utils.js => routes/Moderation/graphql.js} | 57 ++--- .../src/routes/Moderation/queueConfig.js | 35 +++ .../client/containers/ModSubscription.js | 25 +- 7 files changed, 170 insertions(+), 305 deletions(-) rename client/coral-admin/src/{graphql/utils.js => routes/Moderation/graphql.js} (74%) create mode 100644 client/coral-admin/src/routes/Moderation/queueConfig.js diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 4b3ea0f0b..9772ebf17 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -37,27 +37,11 @@ const routes = ( - - - - - - - - - - - - - - - - - - - - + + + +
diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index ce0c31e33..f50ff9c50 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -101,33 +101,19 @@ export default class Moderation extends Component { } render () { - const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props; - const assetId = this.props.params.id; + const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props; const {asset} = root; + const assetId = asset && asset.id; const comments = root[activeTab]; - let activeTabCount; - switch(activeTab) { - case 'all': - activeTabCount = root.allCount; - break; - case 'new': - activeTabCount = root.newCount; - break; - case 'approved': - activeTabCount = root.approvedCount; - break; - case 'premod': - activeTabCount = root.premodCount; - break; - case 'reported': - activeTabCount = root.reportedCount; - break; - case 'rejected': - activeTabCount = root.rejectedCount; - break; - } + const activeTabCount = root[`${activeTab}Count`]; + const menuItems = Object.keys(queueConfig).map((queue) => ({ + key: queue, + name: queueConfig[queue].name, + icon: queueConfig[queue].icon, + count: root[`${queue}Count`] + })); return (
@@ -139,16 +125,10 @@ export default class Moderation extends Component { />
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js index 62bbd4d2e..7b427ac54 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js @@ -9,16 +9,10 @@ import cn from 'classnames'; import t from 'coral-framework/services/i18n'; const ModerationMenu = ({ - asset = {}, - allCount, - approvedCount, - premodCount, - newCount, - rejectedCount, - reportedCount, + asset = {}, + items, selectSort, sort, - premodEnabled, getModPath, activeTab }) => { @@ -27,49 +21,15 @@ const ModerationMenu = ({
- - { - premodEnabled ? ( - - {t('modqueue.premod')} - - ) : ( - - {t('modqueue.new')} - - ) - } - - - {t('modqueue.reported')} - - - {t('modqueue.approved')} - - - {t('modqueue.rejected')} - - - {t('modqueue.all')} - + {items.map((queue) => + + {queue.name} + + )}
{ + return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab); + }; + get activeTab() { - const {root: {asset, settings}, router, route} = this.props; + const {root: {asset, settings}} = this.props; + const id = getAssetId(this.props); + const tab = getTab(this.props); // Grab premod from asset or from settings - const premod = !router.params.id ? settings.moderation : asset.settings.moderation; + const premod = !id ? settings.moderation : asset.settings.moderation; const queue = isPremod(premod) ? 'premod' : 'new'; - const activeTab = route.path && route.path !== ':id' ? route.path : queue; + const activeTab = tab ? tab : queue; return activeTab; } @@ -57,15 +78,10 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => { const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const sort = this.props.moderation.sortOrder; const notify = this.props.auth.user.id === user.id - ? {} - : { - activeQueue: this.activeTab, - text: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)), - anyQueue: false, - }; - return handleCommentChange(prev, comment, sort, notify); + ? '' + : t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notify); }, }); @@ -74,15 +90,10 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => { const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const sort = this.props.moderation.sortOrder; const notify = this.props.auth.user.id === user.id - ? {} - : { - activeQueue: this.activeTab, - text: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)), - anyQueue: false, - }; - return handleCommentChange(prev, comment, sort, notify); + ? '' + : t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notify); }, }); @@ -90,13 +101,8 @@ class ModerationContainer extends Component { document: COMMENT_EDITED_SUBSCRIPTION, variables, updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => { - const sort = this.props.moderation.sortOrder; - const notify = { - activeQueue: this.activeTab, - text: t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)), - anyQueue: false, - }; - return handleCommentChange(prev, comment, sort, notify); + const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notify); }, }); @@ -105,13 +111,8 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => { const user = comment.actions[comment.actions.length - 1].user; - const sort = this.props.moderation.sortOrder; - const notify = { - activeQueue: this.activeTab, - text: t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)), - anyQueue: true, - }; - return handleCommentChange(prev, comment, sort, notify); + const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notify); }, }); @@ -160,28 +161,9 @@ class ModerationContainer extends Component { cursor: this.props.root[tab].endCursor, sort: this.props.data.variables.sort, asset_id: this.props.data.variables.asset_id, + statuses: queueConfig[tab].statuses, + action_type: queueConfig[tab].action_type, }; - switch(tab) { - case 'all': - variables.statuses = null; - break; - case 'new': - variables.statuses = ['NONE', 'PREMOD']; - break; - case 'approved': - variables.statuses = ['ACCEPTED']; - break; - case 'premod': - variables.statuses = ['PREMOD']; - break; - case 'reported': - variables.statuses = ['NONE', 'PREMOD']; - variables.action_type = 'FLAG'; - break; - case 'rejected': - variables.statuses = ['REJECTED']; - break; - } return this.props.data.fetchMore({ query: LOAD_MORE_QUERY, variables, @@ -199,7 +181,8 @@ class ModerationContainer extends Component { }; render () { - const {root, root: {asset, settings}, data, params: {id: assetId}} = this.props; + const {root, root: {asset, settings}, data} = this.props; + const assetId = getAssetId(this.props); if (data.error) { return
Error
; @@ -222,6 +205,14 @@ class ModerationContainer extends Component { return ; } + const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation); + const currentQueueConfig = Object.assign({}, queueConfig); + if (premodEnabled) { + delete currentQueueConfig.new; + } else { + delete currentQueueConfig.premod; + } + return ; } } @@ -314,49 +306,25 @@ const commentConnectionFragment = gql` const withModQueueQuery = withQuery(gql` query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) { - all: comments(query: { - statuses: [NONE, PREMOD, ACCEPTED, REJECTED], - asset_id: $asset_id, - sort: $sort - }) { - ...CoralAdmin_Moderation_CommentConnection - } - new: comments(query: { - statuses: [NONE, PREMOD], - asset_id: $asset_id, - sort: $sort - }) { - ...CoralAdmin_Moderation_CommentConnection - } - approved: comments(query: { - statuses: [ACCEPTED], - asset_id: $asset_id, - sort: $sort - }) { - ...CoralAdmin_Moderation_CommentConnection - } - premod: comments(query: { - statuses: [PREMOD], + ${Object.keys(queueConfig).map((queue) => ` + ${queue}: comments(query: { + ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} + ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} + ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} asset_id: $asset_id, sort: $sort - }) { - ...CoralAdmin_Moderation_CommentConnection - } - reported: comments(query: { - action_type: FLAG, + }) { + ...CoralAdmin_Moderation_CommentConnection + } + `)} + ${Object.keys(queueConfig).map((queue) => ` + ${queue}Count: commentCount(query: { + ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} + ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} + ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} asset_id: $asset_id, - statuses: [NONE, PREMOD], - sort: $sort - }) { - ...CoralAdmin_Moderation_CommentConnection - } - rejected: comments(query: { - statuses: [REJECTED], - asset_id: $asset_id, - sort: $sort - }) { - ...CoralAdmin_Moderation_CommentConnection - } + }) + `)} asset(id: $asset_id) @skip(if: $allAssets) { id title @@ -365,30 +333,6 @@ const withModQueueQuery = withQuery(gql` moderation } } - allCount: commentCount(query: { - asset_id: $asset_id - }) - newCount: commentCount(query: { - statuses: [NONE, PREMOD], - asset_id: $asset_id - }) - approvedCount: commentCount(query: { - statuses: [ACCEPTED], - asset_id: $asset_id - }) - premodCount: commentCount(query: { - statuses: [PREMOD], - asset_id: $asset_id - }) - rejectedCount: commentCount(query: { - statuses: [REJECTED], - asset_id: $asset_id - }) - reportedCount: commentCount(query: { - action_type: FLAG, - asset_id: $asset_id, - statuses: [NONE, PREMOD] - }) settings { organizationName moderation @@ -396,11 +340,12 @@ const withModQueueQuery = withQuery(gql` } ${commentConnectionFragment} `, { - options: ({params: {id = null}, moderation: {sortOrder}}) => { + options: (props) => { + const id = getAssetId(props); return { variables: { asset_id: id, - sort: sortOrder, + sort: props.moderation.sortOrder, allAssets: id === null } }; @@ -409,33 +354,18 @@ const withModQueueQuery = withQuery(gql` const withQueueCountPolling = withQuery(gql` query CoralAdmin_ModerationCountPoll($asset_id: ID) { - allCount: commentCount(query: { - asset_id: $asset_id - }) - newCount: commentCount(query: { - statuses: [NONE, PREMOD], - asset_id: $asset_id - }) - approvedCount: commentCount(query: { - statuses: [ACCEPTED], - asset_id: $asset_id - }) - premodCount: commentCount(query: { - statuses: [PREMOD], - asset_id: $asset_id - }) - rejectedCount: commentCount(query: { - statuses: [REJECTED], - asset_id: $asset_id - }) - reportedCount: commentCount(query: { - action_type: FLAG, - asset_id: $asset_id, - statuses: [NONE, PREMOD] - }) + ${Object.keys(queueConfig).map((queue) => ` + ${queue}Count: commentCount(query: { + ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} + ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} + ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} + asset_id: $asset_id, + }) + `)} } `, { - options: ({params: {id = null}}) => { + options: (props) => { + const id = getAssetId(props); return { pollInterval: 5000, variables: { diff --git a/client/coral-admin/src/graphql/utils.js b/client/coral-admin/src/routes/Moderation/graphql.js similarity index 74% rename from client/coral-admin/src/graphql/utils.js rename to client/coral-admin/src/routes/Moderation/graphql.js index f729cc0ea..0d4991f72 100644 --- a/client/coral-admin/src/graphql/utils.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -1,7 +1,6 @@ import update from 'immutability-helper'; import * as notification from 'coral-admin/src/services/notification'; -const queues = ['all', 'premod', 'reported', 'approved', 'rejected', 'new']; const limit = 10; const ascending = (a, b) => { @@ -67,32 +66,24 @@ function addCommentToQueue(root, queue, comment, sort) { /** * getCommentQueues determines in which queues a comment should be placed. */ -function getCommentQueues(comment) { - const queues = ['all']; - const isFlagged = comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction'); - - switch(comment.status) { - case 'ACCEPTED': - queues.push('approved'); - break; - case 'REJECTED': - queues.push('rejected'); - break; - case 'PREMOD': - queues.push('premod'); - queues.push('new'); - if (isFlagged) { - queues.push('reported'); +function getCommentQueues(comment, queueConfig) { + const queues = []; + Object.keys(queueConfig).forEach((key) => { + const {action_type, statuses, tags} = queueConfig[key]; + let addToQueues = false; + if (statuses && statuses.indexOf(comment.status) >= 0) { + addToQueues = true; } - break; - case 'NONE': - queues.push('new'); - if (isFlagged) { - queues.push('reported'); + if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) { + addToQueues = true; } - break; - } - + if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) { + addToQueues = true; + } + if (addToQueues) { + queues.push(key); + } + }); return queues; } @@ -106,42 +97,42 @@ function getCommentQueues(comment) { * @param {string} notify.text notification text to show * @param {bool} notify.anyQueue if true show the notification when the comment is shown * in the current active queue besides the 'all' queue. + * @param {Object} queueConfig queue configuration * @return {Object} next state of the store */ -export function handleCommentChange(root, comment, sort, notify) { +export function handleCommentChange(root, comment, sort, notify, queueConfig, activeQueue) { let next = root; - const nextQueues = getCommentQueues(comment); + const nextQueues = getCommentQueues(comment, queueConfig); let notificationShown = false; const showNotificationOnce = () => { if (notificationShown) { return; } - notification.info(notify.text); + notification.info(notify); notificationShown = true; }; - queues.forEach((queue) => { + Object.keys(queueConfig).forEach((queue) => { if (nextQueues.indexOf(queue) >= 0) { if (!queueHasComment(next, queue, comment.id)) { next = addCommentToQueue(next, queue, comment, sort); - if (notify && notify.activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) { + if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) { showNotificationOnce(comment); } } } else if(queueHasComment(next, queue, comment.id)){ next = removeCommentFromQueue(next, queue, comment.id); - if (notify && notify.activeQueue === queue) { + if (notify && activeQueue === queue) { showNotificationOnce(comment); } } if ( notify - && (queue === 'all' || notify.anyQueue) && queueHasComment(next, queue, comment.id) - && notify.activeQueue === queue + && activeQueue === queue ) { showNotificationOnce(comment); } diff --git a/client/coral-admin/src/routes/Moderation/queueConfig.js b/client/coral-admin/src/routes/Moderation/queueConfig.js new file mode 100644 index 000000000..5385b5e23 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/queueConfig.js @@ -0,0 +1,35 @@ +import t from 'coral-framework/services/i18n'; + +export default { + premod: { + statuses: ['PREMOD'], + icon: 'access_time', + name: t('modqueue.premod'), + }, + new: { + statuses: ['NONE', 'PREMOD'], + icon: 'question_answer', + name: t('modqueue.new'), + }, + reported: { + action_type: 'FLAG', + statuses: ['NONE', 'PREMOD'], + icon: 'flag', + name: t('modqueue.reported'), + }, + approved: { + statuses: ['ACCEPTED'], + icon: 'check', + name: t('modqueue.approved'), + }, + rejected: { + statuses: ['REJECTED'], + icon: 'close', + name: t('modqueue.rejected'), + }, + all: { + statuses: ['NONE', 'PREMOD', 'ACCEPTED', 'REJECTED'], + icon: 'question_answer', + name: t('modqueue.all'), + } +}; diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js index 647c7ecfd..85e0d8fe2 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js @@ -2,7 +2,6 @@ import React from 'react'; import {gql} from 'react-apollo'; import {connect} from 'react-redux'; import Comment from 'coral-admin/src/routes/Moderation/containers/Comment'; -import {handleCommentChange} from 'coral-admin/src/graphql/utils'; import {getDefinitionName} from 'coral-framework/utils'; import truncate from 'lodash/truncate'; import t from 'coral-framework/services/i18n'; @@ -22,20 +21,14 @@ class ModSubscription extends React.Component { assetId: this.props.data.variables.asset_id, }, updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => { - const sort = this.props.data.variables.sort; - const text = this.props.user.id === user.id - ? {} + const notify = this.props.user.id === user.id + ? '' : t( 'talk-plugin-featured-comments.notify_featured', user.username, prepareNotificationText(comment.body), ); - const notify = { - activeQueue: this.props.activeTab, - text, - anyQueue: true, - }; - return handleCommentChange(prev, comment, sort, notify); + return this.props.handleCommentChange(prev, comment, notify); }, }, { @@ -44,20 +37,14 @@ class ModSubscription extends React.Component { assetId: this.props.data.variables.asset_id, }, updateQuery: (prev, {subscriptionData: {data: {commentUnfeatured: {user, comment}}}}) => { - const sort = this.props.data.variables.sort; - const text = this.props.user.id === user.id - ? {} + const notify = this.props.user.id === user.id + ? '' : t( 'talk-plugin-featured-comments.notify_unfeatured', user.username, prepareNotificationText(comment.body), ); - const notify = { - activeQueue: this.props.activeTab, - text, - anyQueue: true, - }; - return handleCommentChange(prev, comment, sort, notify); + return this.props.handleCommentChange(prev, comment, notify); } }, ]; From 6956b4cd3102112ee3eff9b1d8400d8c260754c7 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 10 Aug 2017 18:39:02 +0700 Subject: [PATCH 025/109] Remove Tag should keep remaining tags --- services/tags.js | 4 ++-- test/server/services/tags.js | 42 ++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/services/tags.js b/services/tags.js index fbe6b58ec..b0d0c4b96 100644 --- a/services/tags.js +++ b/services/tags.js @@ -205,8 +205,8 @@ class TagsService { return updateModel(item_type, query, { $pull: { tags: { - name: link.tag.name - } + 'tag.name': link.tag.name, + }, } }); } diff --git a/test/server/services/tags.js b/test/server/services/tags.js index 7cdad829e..1080563f0 100644 --- a/test/server/services/tags.js +++ b/test/server/services/tags.js @@ -27,7 +27,7 @@ describe('services.TagsService', () => { const id = comment.id; const name = 'BEST'; const assigned_by = user.id; - + await TagsService.add(id, 'COMMENTS', { tag: { name @@ -45,7 +45,7 @@ describe('services.TagsService', () => { const id = comment.id; const name = 'BEST'; const assigned_by = user.id; - + await TagsService.add(id, 'COMMENTS', { tag: { name @@ -103,5 +103,43 @@ describe('services.TagsService', () => { expect(tags.length).to.equal(0); } }); + it('removes a tag out of 2', async () => { + const id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + tag: { + name: 'ANOTHER' + }, + assigned_by + }); + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(2); + } + + // ok now to remove it + await TagsService.remove(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + expect(tags[0].tag.name).to.equal('ANOTHER'); + } + }); }); }); From 6794827a7730a44d58315ebd0aa51bc2adfbd334 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 10 Aug 2017 20:23:31 +0700 Subject: [PATCH 026/109] Support adding modqueues from plugins --- client/coral-admin/src/routes/Moderation/queueConfig.js | 4 +++- client/coral-framework/helpers/plugins.js | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/queueConfig.js b/client/coral-admin/src/routes/Moderation/queueConfig.js index 5385b5e23..b9e9e5320 100644 --- a/client/coral-admin/src/routes/Moderation/queueConfig.js +++ b/client/coral-admin/src/routes/Moderation/queueConfig.js @@ -1,4 +1,5 @@ import t from 'coral-framework/services/i18n'; +import {getModQueueConfigs} from 'coral-framework/helpers/plugins'; export default { premod: { @@ -31,5 +32,6 @@ export default { statuses: ['NONE', 'PREMOD', 'ACCEPTED', 'REJECTED'], icon: 'question_answer', name: t('modqueue.all'), - } + }, + ...getModQueueConfigs(), }; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 5e5a8c0c0..b2ca78142 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -3,6 +3,7 @@ import uniq from 'lodash/uniq'; import pick from 'lodash/pick'; import merge from 'lodash/merge'; import flattenDeep from 'lodash/flattenDeep'; +import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers} from 'coral-framework/services/store'; @@ -64,7 +65,13 @@ export function getSlotFragments(slot, part) { export function getGraphQLExtensions() { return plugins .map((o) => pick(o.module, ['mutations', 'queries', 'fragments'])) - .filter((o) => o); + .filter((o) => !isEmpty(o)); +} + +export function getModQueueConfigs() { + return merge(...plugins + .map((o) => o.module.modQueues) + .filter((o) => o)); } function getTranslations() { From d008947b4175d6eb018d20efdcdfc34366f0c572 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 10 Aug 2017 15:51:50 +0100 Subject: [PATCH 027/109] Add important links to README --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 89aaefee8..a58ac7c74 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,21 @@ Online comments are broken. Our open-source Talk tool rethinks how moderation, c Third party licenses are available via the `/client/3rdpartylicenses.txt` endpoint when the server is running with built assets. -## Documentation +## Important Links -See our [Talk Documentation & Guides](https://coralproject.github.io/talk/). +- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/ + +- Pivotal Tracker Backlog & Release Schedule: https://www.pivotaltracker.com/n/projects/1863625 + +## Learn More about Coral + +- Community Forums: https://community.coralproject.net/ + +- Website: https://coralproject.net + +- Blog: https://blog.coralproject.net + +- Community Guides for Journalism: https://guides.coralproject.net/ ## License From ada5e6f747c7740967f6488e11fbb6cf555479b4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 11 Aug 2017 19:53:08 +0700 Subject: [PATCH 028/109] Update apollo and fix warnings --- .../coral-embed-stream/src/graphql/index.js | 10 ++- package.json | 4 +- yarn.lock | 84 +++++++++++-------- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index f52cc4e7d..2c39235a2 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -152,8 +152,6 @@ const extension = { }, created_at: new Date().toISOString(), body, - parent_id, - asset_id, action_summaries: [], tags: tags.map((tag) => ({ tag: { @@ -169,8 +167,14 @@ const extension = { })), status: 'NONE', replyCount: 0, + asset: { + __typename: 'Asset', + id: asset_id, + title: '', + url: '', + }, parent: parent_id - ? {id: parent_id} + ? {__typename: 'Comment', id: parent_id} : null, replies: { __typename: 'CommentConnection', diff --git a/package.json b/package.json index bb0a724ac..ebe1138f6 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "passport-local": "^1.0.0", "prop-types": "^15.5.10", "query-strings": "^0.0.1", - "react-apollo": "^1.1.0", + "react-apollo": "^1.4.12", "react-input-autosize": "^1.1.4", "react-recaptcha": "^2.2.6", "react-toastify": "^1.5.0", @@ -136,7 +136,7 @@ "yamljs": "^0.2.10" }, "devDependencies": { - "apollo-client": "^1.0.4", + "apollo-client": "^1.9.1", "autoprefixer": "^6.5.2", "babel-cli": "^6.24.0", "babel-core": "^6.24.0", diff --git a/yarn.lock b/yarn.lock index a5d73a756..9c1ac35e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,10 +9,6 @@ git-url-parse "^6.0.2" shelljs "^0.7.0" -"@types/async@^2.0.31": - version "2.0.40" - resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.40.tgz#ac02de68e66c004a61b7cb16df8b1db3a254cca9" - "@types/express-serve-static-core@*": version "4.0.44" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.44.tgz#a1c3bd5d80e93c72fba91a03f5412c47f21d4ae7" @@ -26,18 +22,18 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/graphql@0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.10.2.tgz#d7c79acbaa17453b6681c80c34b38fcb10c4c08c" + "@types/graphql@^0.8.5", "@types/graphql@^0.8.6": version "0.8.6" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68" -"@types/graphql@^0.9.0", "@types/graphql@^0.9.1": +"@types/graphql@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.1.tgz#b04ebe84bc997cc60dbea2ed4d0d4342c737f99d" -"@types/isomorphic-fetch@0.0.33": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.33.tgz#3ea1b86f8b73e6a7430d01d4dbd5b1f63fd72718" - "@types/mime@*": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" @@ -203,20 +199,27 @@ anymatch@^1.3.0: arrify "^1.0.0" micromatch "^2.1.5" -apollo-client@^1.0.2, apollo-client@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.4.tgz#af75db8cdd27e08a835ddfb39807849e178540f9" +apollo-client@^1.4.0, apollo-client@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.9.1.tgz#9e6a383605572c755038cf5d7fdac9382bcdc040" dependencies: - graphql "^0.9.3" + apollo-link-core "^0.5.0" + graphql "^0.10.0" graphql-anywhere "^3.0.1" graphql-tag "^2.0.0" redux "^3.4.0" symbol-observable "^1.0.2" whatwg-fetch "^2.0.0" optionalDependencies: - "@types/async" "^2.0.31" - "@types/graphql" "^0.9.0" - "@types/isomorphic-fetch" "0.0.33" + "@types/graphql" "0.10.2" + +apollo-link-core@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/apollo-link-core/-/apollo-link-core-0.5.0.tgz#dc87da1aaa63b029321ae70938dc26257f5ab8c6" + dependencies: + graphql "^0.10.3" + graphql-tag "^2.4.2" + zen-observable-ts "^0.4.0" app-module-path@^2.2.0: version "2.2.0" @@ -3711,6 +3714,10 @@ graphql-tag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.0.0.tgz#f3efe3b4d64f33bfe8479ae06a461c9d72f2a6fe" +graphql-tag@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.4.2.tgz#6a63297d8522d03a2b72d26f1b239aab343840cd" + graphql-tools@^0.10.1: version "0.10.1" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-0.10.1.tgz#274aa338d50b1c0b3ed6936eafd8ed3a19ed1828" @@ -3721,13 +3728,19 @@ graphql-tools@^0.10.1: optionalDependencies: "@types/graphql" "^0.8.5" +graphql@^0.10.0, graphql@^0.10.3: + version "0.10.5" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.10.5.tgz#c9be17ca2bdfdbd134077ffd9bbaa48b8becd298" + dependencies: + iterall "^1.1.0" + graphql@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.7.2.tgz#cc894a32823399b8a0cb012b9e9ecad35cd00f72" dependencies: iterall "1.0.2" -graphql@^0.9.1, graphql@^0.9.3: +graphql@^0.9.1: version "0.9.3" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.9.3.tgz#71fc0fa331bffb9c20678485861cfb370803118e" dependencies: @@ -3876,6 +3889,10 @@ hoist-non-react-statics@^1.0.0, hoist-non-react-statics@^1.0.3, hoist-non-react- version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" +hoist-non-react-statics@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.2.2.tgz#c0eca5a7d5a28c5ada3107eb763b01da6bfa81fb" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -3996,11 +4013,11 @@ iconv-lite@0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" -iconv-lite@0.4.15, iconv-lite@^0.4.5, iconv-lite@~0.4.13: +iconv-lite@0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" -iconv-lite@^0.4.17: +iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13: version "0.4.18" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" @@ -4531,7 +4548,7 @@ iterall@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.3.tgz#e0b31958f835013c323ff0b10943829ac69aa4b7" -iterall@^1.1.1: +iterall@^1.1.0, iterall@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" @@ -6912,14 +6929,14 @@ react-addons-test-utils@^15.4.2: fbjs "^0.8.4" object-assign "^4.1.0" -react-apollo@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.1.1.tgz#136a6be6e0ed7bfa5292e67073e1829fe12e1e17" +react-apollo@^1.4.12: + version "1.4.12" + resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.4.12.tgz#0cacbdd335acec4c1079feb48047df1c43f77bdf" dependencies: - apollo-client "^1.0.2" + apollo-client "^1.4.0" graphql-anywhere "^3.0.0" graphql-tag "^2.0.0" - hoist-non-react-statics "^1.2.0" + hoist-non-react-statics "^2.2.0" invariant "^2.2.1" lodash.flatten "^4.2.0" lodash.isequal "^4.1.1" @@ -6927,10 +6944,8 @@ react-apollo@^1.1.0: lodash.pick "^4.4.0" object-assign "^4.0.1" prop-types "^15.5.8" - optionalDependencies: - react-dom "0.14.x || 15.* || ^15.0.0" -"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@^15.3.1, react-dom@^15.4.2: +react-dom@^15.3.1, react-dom@^15.4.2: version "15.5.4" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da" dependencies: @@ -7869,14 +7884,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" - -string-width@^2.1.0: +string-width@^2.0.0, string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: @@ -8816,3 +8824,7 @@ yauzl@^2.5.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" + +zen-observable-ts@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.4.0.tgz#a74bc9fe59747948a577bd513d438e70fcfae7e2" From 17bc396b06fea740c11d28177031f500d770570d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 11 Aug 2017 20:55:23 +0700 Subject: [PATCH 029/109] Fix Ignored User in permalink --- client/coral-embed-stream/style/default.css | 2 +- client/coral-framework/hocs/withMutation.js | 4 ++-- client/coral-framework/hocs/withQuery.js | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 802cd2e5b..6ba376096 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -13,7 +13,7 @@ body { width: 100%; font-size: 14px; margin: 0px; - padding: 0px 0px 50px 0px; + padding: 0px 0px 100px 0px; height: auto !important; } diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index c9108c0fe..7e2c3d09d 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -92,13 +92,13 @@ export default (document, config = {}) => (WrappedComponent) => { // Do not run updates when we have mutation errors. return prev; } - return map[key](prev, result); + return map[key](prev, result) || prev; }; } else { const existing = res[key]; res[key] = (prev, result) => { const next = existing(prev, result); - return map[key](next, result); + return map[key](next, result) || next; }; } }); diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index adb67b7b3..82c9c29d4 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -128,8 +128,10 @@ export default (document, config = {}) => (WrappedComponent) => { const reducer = withSkipOnErrors( reducerCallbacks.reduce( - (a, b) => (prev, ...rest) => - b(a(prev, ...rest), ...rest), + (a, b) => (prev, ...rest) => { + const next = a(prev, ...rest); + return b(next, ...rest) || next; + } )); return { From 45cf72256beab523c779e54fc547a2f4a13115a5 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 11 Aug 2017 21:11:59 +0700 Subject: [PATCH 030/109] Fix dashboard links --- .../src/routes/Dashboard/components/ActivityWidget.js | 6 +++--- .../src/routes/Dashboard/components/FlagWidget.js | 6 +++--- .../src/routes/Dashboard/components/LikeWidget.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js index 5f974971c..d92b2bd14 100644 --- a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js @@ -17,11 +17,11 @@ const ActivityWidget = ({assets}) => { ? assets.map((asset) => { return (
- Moderate + Moderate

{asset.commentCount}

- +

{asset.title}

- +

{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

); diff --git a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js index ef0974fbd..ef4a6857e 100644 --- a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js @@ -24,11 +24,11 @@ const FlagWidget = ({assets}) => { return (
- Moderate + Moderate

{flagSummary ? flagSummary.actionCount : 0}

- +

{asset.title}

- +

{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

); diff --git a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js index 2188e38dd..a61f888bf 100644 --- a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js @@ -19,11 +19,11 @@ const LikeWidget = ({assets}) => { const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary'); return (
- Moderate + Moderate

{likeSummary ? likeSummary.actionCount : 0}

- +

{asset.title}

- +

{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

); From 7360a43898878f3ce7314841c8daf7ee60c7c24f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 11 Aug 2017 22:50:12 +0700 Subject: [PATCH 031/109] Remove immutable from embed stream --- .../containers/ConfigureStreamContainer.js | 2 +- .../src/containers/Embed.js | 2 +- .../src/containers/Stream.js | 5 +- .../coral-embed-stream/src/graphql/index.js | 6 +- client/coral-framework/actions/asset.js | 4 +- client/coral-framework/actions/auth.js | 27 +- client/coral-framework/actions/user.js | 21 -- client/coral-framework/constants/assets.js | 3 - client/coral-framework/constants/user.js | 10 - client/coral-framework/reducers/asset.js | 19 +- client/coral-framework/reducers/auth.js | 281 +++++++++++------- client/coral-framework/reducers/index.js | 2 - client/coral-framework/reducers/user.js | 43 --- .../containers/ProfileContainer.js | 11 +- plugin-api/beta/client/hocs/withReaction.js | 2 +- plugin-api/beta/client/hocs/withTags.js | 2 +- .../client/components/ChangeUsername.js | 2 +- .../client/components/SignInButton.js | 2 +- .../client/components/SignInContainer.js | 2 +- .../client/components/UserBox.js | 4 +- 20 files changed, 236 insertions(+), 214 deletions(-) delete mode 100644 client/coral-framework/actions/user.js delete mode 100644 client/coral-framework/constants/assets.js delete mode 100644 client/coral-framework/constants/user.js delete mode 100644 client/coral-framework/reducers/user.js diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index e7b937030..8489c809f 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -119,7 +119,7 @@ class ConfigureStreamContainer extends Component { } const mapStateToProps = (state) => ({ - asset: state.asset.toJS() + asset: state.asset }); const mapDispatchToProps = (dispatch) => ({ diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 06646a6c2..e21a683d1 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -170,7 +170,7 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, { }); const mapStateToProps = (state) => ({ - auth: state.auth.toJS(), + auth: state.auth, commentId: state.stream.commentId, assetId: state.stream.assetId, assetUrl: state.stream.assetUrl, diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 73676713c..451b115a9 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -10,7 +10,6 @@ import { import * as authActions from 'coral-framework/actions/auth'; import * as notificationActions from 'coral-framework/actions/notification'; -import {editName} from 'coral-framework/actions/user'; import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream'; import Stream from '../components/Stream'; import Comment from './Comment'; @@ -26,7 +25,7 @@ import { } from '../graphql/utils'; import omit from 'lodash/omit'; -const {showSignInDialog} = authActions; +const {showSignInDialog, editName} = authActions; const {addNotification} = notificationActions; class StreamContainer extends React.Component { @@ -298,7 +297,7 @@ const fragments = { }; const mapStateToProps = (state) => ({ - auth: state.auth.toJS(), + auth: state.auth, refetching: state.embed.refetching, commentCountCache: state.stream.commentCountCache, activeReplyBox: state.stream.activeReplyBox, diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index f52cc4e7d..4cd4a5d23 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -147,8 +147,8 @@ const extension = { __typename: 'Comment', user: { __typename: 'User', - id: auth.toJS().user.id, - username: auth.toJS().user.username + id: auth.user.id, + username: auth.user.username }, created_at: new Date().toISOString(), body, @@ -162,7 +162,7 @@ const extension = { __typename: 'Tag' }, assigned_by: { - id: auth.toJS().user.id, + id: auth.user.id, __typename: 'User' }, __typename: 'TagLink' diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js index 24739ddde..aab3caf3a 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-framework/actions/asset.js @@ -13,7 +13,7 @@ const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SE const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error}); export const updateConfiguration = (newConfig) => (dispatch, getState) => { - const assetId = getState().asset.toJS().id; + const assetId = getState().asset.id; dispatch(updateAssetSettingsRequest()); coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) .then(() => { @@ -27,7 +27,7 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => { }; export const updateOpenStream = (closedBody) => (dispatch, getState) => { - const assetId = getState().asset.toJS().id; + const assetId = getState().asset.id; dispatch(fetchAssetRequest()); coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) .then(() => { diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index f4cf5217e..7c6d3bf89 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -4,6 +4,7 @@ import * as actions from '../constants/auth'; import * as Storage from '../helpers/storage'; import coralApi, {base} from '../helpers/request'; import pym from '../services/pym'; +import {addNotification} from '../actions/notification'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; @@ -220,7 +221,7 @@ const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error}); export const fetchSignUp = (formData) => (dispatch, getState) => { - const redirectUri = getState().auth.toJS().redirectUri; + const redirectUri = getState().auth.redirectUri; dispatch(signUpRequest()); coralApi('/users', { @@ -257,7 +258,7 @@ const forgotPasswordFailure = (error) => ({ export const fetchForgotPassword = (email) => (dispatch, getState) => { dispatch(forgotPasswordRequest(email)); - const redirectUri = getState().auth.toJS().redirectUri; + const redirectUri = getState().auth.redirectUri; coralApi('/account/password/reset', { method: 'POST', body: {email, loc: redirectUri} @@ -351,7 +352,7 @@ const verifyEmailFailure = () => ({ }); export const requestConfirmEmail = (email) => (dispatch, getState) => { - const redirectUri = getState().auth.toJS().redirectUri; + const redirectUri = getState().auth.redirectUri; dispatch(verifyEmailRequest()); return coralApi('/users/resend-verify', { method: 'POST', @@ -378,3 +379,23 @@ export const setRedirectUri = (uri) => ({ type: actions.SET_REDIRECT_URI, uri, }); + +//============================================================================== +// Edit Username +//============================================================================== + +const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error}); +const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS}); + +export const editName = (username) => (dispatch) => { + return coralApi('/account/username', {method: 'PUT', body: {username}}) + .then(() => { + dispatch(editUsernameSuccess()); + dispatch(addNotification('success', t('framework.success_name_update'))); + }) + .catch((error) => { + console.error(error); + const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); + dispatch(editUsernameFailure(errorMessage)); + }); +}; diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js deleted file mode 100644 index a6ad45d4a..000000000 --- a/client/coral-framework/actions/user.js +++ /dev/null @@ -1,21 +0,0 @@ -import {addNotification} from '../actions/notification'; -import coralApi from '../helpers/request'; -import * as actions from '../constants/auth'; - -import t from 'coral-framework/services/i18n'; - -const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error}); -const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS}); - -export const editName = (username) => (dispatch) => { - return coralApi('/account/username', {method: 'PUT', body: {username}}) - .then(() => { - dispatch(editUsernameSuccess()); - dispatch(addNotification('success', t('framework.success_name_update'))); - }) - .catch((error) => { - console.error(error); - const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch(editUsernameFailure(errorMessage)); - }); -}; diff --git a/client/coral-framework/constants/assets.js b/client/coral-framework/constants/assets.js deleted file mode 100644 index 3883ee835..000000000 --- a/client/coral-framework/constants/assets.js +++ /dev/null @@ -1,3 +0,0 @@ -export const MULTIPLE_ASSETS_REQUEST = 'MULTIPLE_ASSETS_REQUEST'; -export const MULTIPLE_ASSETS_SUCCESS = 'MULTIPLE_ASSETS_SUCCESS'; -export const MULTIPLE_ASSSETS_FAILURE = 'MULTIPLE_ASSSETS_FAILURE'; diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js deleted file mode 100644 index 1557a42c9..000000000 --- a/client/coral-framework/constants/user.js +++ /dev/null @@ -1,10 +0,0 @@ -export const EDIT_NAME_REQUEST = 'EDIT_NAME_REQUEST'; -export const EDIT_NAME_SUCCESS = 'EDIT_NAME_SUCCESS'; -export const EDIT_NAME_FAILURE = 'EDIT_NAME_FAILURE'; -export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST'; -export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS'; -export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; -export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; -export const UPDATE_USERNAME = 'UPDATE_USERNAME'; -export const IGNORE_USER_SUCCESS = 'IGNORE_USER_SUCCESS'; -export const STOP_IGNORING_USER_SUCCESS = 'STOP_IGNORING_USER_SUCCESS'; diff --git a/client/coral-framework/reducers/asset.js b/client/coral-framework/reducers/asset.js index f9d0a55e3..b9837eddd 100644 --- a/client/coral-framework/reducers/asset.js +++ b/client/coral-framework/reducers/asset.js @@ -1,24 +1,27 @@ -import {Map} from 'immutable'; import * as actions from '../constants/asset'; -const initialState = Map({ +const initialState = { closedAt: null, settings: null, title: null, url: null, - features: Map({}), + features: {}, status: 'open', moderation: null -}); +}; export default function asset (state = initialState, action) { switch (action.type) { case actions.FETCH_ASSET_SUCCESS: - return state - .merge(action.asset); + return { + ...state, + ...action.asset, + }; case actions.UPDATE_ASSET_SETTINGS_SUCCESS: - return state - .setIn(['settings'], action.settings); + return { + ...state, + settings: action.settings, + }; default: return state; } diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 5481b7a5e..1e18ddffa 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -1,8 +1,7 @@ -import {Map, fromJS} from 'immutable'; import * as actions from '../constants/auth'; import pym from 'coral-framework/services/pym'; -const initialState = Map({ +const initialState = { isLoading: false, loggedIn: false, user: null, @@ -21,27 +20,34 @@ const initialState = Map({ fromSignUp: false, requireEmailConfirmation: false, redirectUri: pym.parentUrl || location.href, -}); +}; const purge = (user) => { - const {settings, profiles, ...userData} = user; // eslint-disable-line - return fromJS(userData); + const {settings, ...userData} = user; // eslint-disable-line + return userData; }; export default function auth (state = initialState, action) { switch (action.type) { case actions.FOCUS_SIGNIN_DIALOG: - return state - .set('signInDialogFocus', true); + return { + ...state, + signInDialogFocus: true, + }; case actions.BLUR_SIGNIN_DIALOG: - return state - .set('signInDialogFocus', false); - case actions.SHOW_SIGNIN_DIALOG : - return state - .set('showSignInDialog', true) - .set('signInDialogFocus', true); + return { + ...state, + signInDialogFocus: false, + }; + case actions.SHOW_SIGNIN_DIALOG: + return { + ...state, + showSignInDialog: true, + signInDialogFocus: true, + }; case actions.HIDE_SIGNIN_DIALOG : - return state.merge(Map({ + return { + ...state, isLoading: false, showSignInDialog: false, signInDialogFocus: false, @@ -53,125 +59,198 @@ export default function auth (state = initialState, action) { emailVerificationSuccess: false, emailVerificationLoading: false, successSignUp: false - })); - case actions.SHOW_CREATEUSERNAME_DIALOG : - return state - .set('showCreateUsernameDialog', true); - case actions.HIDE_CREATEUSERNAME_DIALOG : - return state.merge(Map({ - showCreateUsernameDialog: false - })); - case actions.CREATE_USERNAME_SUCCESS : - return state.merge(Map({ + }; + case actions.SHOW_CREATEUSERNAME_DIALOG: + return { + ...state, + showCreateUsernameDialog: true, + }; + case actions.HIDE_CREATEUSERNAME_DIALOG: + return { + ...state, showCreateUsernameDialog: false, - error: '' - })); - case actions.CREATE_USERNAME_FAILURE : - return state - .set('error', action.error); - case actions.CHANGE_VIEW : - return state - .set('error', '') - .set('view', action.view); + }; + case actions.CREATE_USERNAME_SUCCESS: + return { + ...state, + showCreateUsernameDialog: false, + error: '', + }; + case actions.CREATE_USERNAME_FAILURE: + return { + ...state, + error: action.error, + }; + case actions.CHANGE_VIEW: + return { + ...state, + error: action.error, + view: action.view, + }; case actions.CLEAN_STATE: return initialState; case actions.FETCH_SIGNIN_REQUEST: - return state - .set('isLoading', true); + return { + ...state, + isLoading: true, + }; case actions.CHECK_LOGIN_FAILURE: - return state - .set('checkedInitialLogin', true) - .set('loggedIn', false) - .set('user', null); + return { + ...state, + checkedInitialLogin: true, + loggedIn: false, + user: null, + }; case actions.CHECK_LOGIN_SUCCESS: - return state - .set('checkedInitialLogin', true) - .set('loggedIn', true) - .set('user', purge(action.user)); + return { + ...state, + checkedInitialLogin: true, + loggedIn: true, + user: purge(action.user), + }; case actions.FETCH_SIGNIN_SUCCESS: - return state - .set('loggedIn', true) - .set('user', purge(action.user)); + return { + ...state, + loggedIn: true, + user: purge(action.user), + }; case actions.FETCH_SIGNIN_FAILURE: - return state - .set('isLoading', false) - .set('error', action.error) - .set('user', null); + return { + ...state, + isLoading: false, + error: action.error, + user: null, + }; case actions.FETCH_SIGNUP_FACEBOOK_REQUEST: - return state - .set('fromSignUp', true); + return { + ...state, + fromSignUp: true, + }; case actions.FETCH_SIGNIN_FACEBOOK_REQUEST: - return state - .set('fromSignUp', false); + return { + ...state, + fromSignUp: false, + }; case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS: - return state - .set('user', purge(action.user)) - .set('loggedIn', true); + return { + ...state, + loggedIn: true, + user: purge(action.user), + }; case actions.FETCH_SIGNIN_FACEBOOK_FAILURE: - return state - .set('error', action.error) - .set('user', null); + return { + ...state, + error: action.error, + user: null, + }; case actions.FETCH_SIGNUP_REQUEST: - return state - .set('isLoading', true); + return { + ...state, + isLoading: true, + }; case actions.FETCH_SIGNUP_FAILURE: - return state - .set('error', action.error) - .set('isLoading', false); + return { + ...state, + error: action.error, + isLoading: false, + }; case actions.FETCH_SIGNUP_SUCCESS: - return state - .set('isLoading', false) - .set('successSignUp', true); + return { + ...state, + isLoading: false, + successSignUp: true, + }; case actions.LOGOUT: - return state - .set('user', null) - .set('isLoading', false) - .set('loggedIn', false); + return { + ...state, + user: null, + isLoading: false, + loggedIn: false, + }; case actions.INVALID_FORM: - return state - .set('error', action.error); + return { + ...state, + error: action.error, + }; case actions.VALID_FORM: - return state - .set('error', ''); + return { + ...state, + error: '', + }; case actions.FETCH_FORGOT_PASSWORD_SUCCESS: - return state - .set('passwordRequestFailure', null) - .set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email'); + return { + ...state, + passwordRequestFailure: null, + passwordRequestSuccess: 'If you have a registered account, a password reset link was sent to that email', + }; case actions.FETCH_FORGOT_PASSWORD_FAILURE: - return state - .set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!') - .set('passwordRequestSuccess', null); + return { + ...state, + passwordRequestFailure: 'There was an error sending your password reset email. Please try again soon!', + passwordRequestSuccess: null, + }; case actions.UPDATE_USERNAME: - return state - .setIn(['user', 'username'], action.username); + return { + ...state, + user: { + ...state.user, + username: action.username, + } + }; case actions.VERIFY_EMAIL_FAILURE: - return state - .set('emailVerificationFailure', true) - .set('emailVerificationLoading', false); + return { + ...state, + emailVerificationFailure: true, + emailVerificationLoading: false, + }; case actions.VERIFY_EMAIL_REQUEST: - return state.set('emailVerificationLoading', true); + return { + ...state, + emailVerificationLoading: true, + }; case actions.VERIFY_EMAIL_SUCCESS: - return state - .set('emailVerificationSuccess', true) - .set('emailVerificationLoading', false); + return { + ...state, + emailVerificationSuccess: true, + emailVerificationLoading: false, + }; case actions.SET_REQUIRE_EMAIL_VERIFICATION: - return state - .set('requireEmailConfirmation', action.required); + return { + ...state, + requireEmailConfirmation: action.required, + }; case actions.SET_REDIRECT_URI: - return state - .set('redirectUri', action.uri); + return { + ...state, + redirectUri: action.uri, + }; case 'APOLLO_SUBSCRIPTION_RESULT': if (action.operationName === 'UserBanned' && state.getIn(['user', 'id']) === action.variables.user_id) { - return state - .mergeIn(['user'], action.result.data.userBanned); + return { + ...state, + user: { + ...state.user, + ...action.result.data.userBanned, + }, + }; } if (action.operationName === 'UserSuspended' && state.getIn(['user', 'id']) === action.variables.user_id) { - return state - .mergeIn(['user'], action.result.data.userSuspended); + return { + ...state, + user: { + ...state.user, + ...action.result.data.userSuspended, + }, + }; } if (action.operationName === 'UsernameRejected' && state.getIn(['user', 'id']) === action.variables.user_id) { - return state - .mergeIn(['user'], action.result.data.usernameRejected); + return { + ...state, + user: { + ...state.user, + ...action.result.data.usernameRejected, + }, + }; } return state; default : diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js index f1ac580dc..6b9b730ca 100644 --- a/client/coral-framework/reducers/index.js +++ b/client/coral-framework/reducers/index.js @@ -1,11 +1,9 @@ import auth from './auth'; -import user from './user'; import asset from './asset'; import {reducer as commentBox} from '../../talk-plugin-commentbox'; export default { auth, - user, asset, commentBox, }; diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js deleted file mode 100644 index bc40cf3ae..000000000 --- a/client/coral-framework/reducers/user.js +++ /dev/null @@ -1,43 +0,0 @@ -import {Map} from 'immutable'; -import * as authActions from '../constants/auth'; -import * as actions from '../constants/user'; -import * as assetActions from '../constants/assets'; - -const initialState = Map({ - username: '', - profiles: [], - settings: {}, - myComments: [], - myAssets: [], // the assets from which myComments (above) originated -}); - -const purge = (user) => { - const {_id, created_at, updated_at, __v, roles, ...userData} = user; // eslint-disable-line - return userData; -}; - -export default function user (state = initialState, action) { - switch (action.type) { - case authActions.CHECK_LOGIN_SUCCESS: - return state.merge(Map(purge(action.user))); - case authActions.CHECK_LOGIN_FAILURE: - return initialState; - case authActions.FETCH_SIGNIN_SUCCESS: - return state.merge(Map(purge(action.user))); - case authActions.FETCH_SIGNIN_FAILURE: - return initialState; - case authActions.FETCH_SIGNIN_FACEBOOK_SUCCESS: - return state.merge(Map(purge(action.user))); - case authActions.FETCH_SIGNIN_FACEBOOK_FAILURE: - return initialState; - case actions.SAVE_BIO_SUCCESS: - return state.set('settings', action.settings); - case actions.COMMENTS_BY_USER_SUCCESS: - return state.set('myComments', action.comments); - case assetActions.MULTIPLE_ASSETS_SUCCESS: - return state.set('myAssets', action.assets); - case actions.LOGOUT_SUCCESS: - return initialState; - } - return state; -} diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 0090847f7..e33c0b6cd 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -51,7 +51,7 @@ class ProfileContainer extends Component { }; render() { - const {auth, asset, showSignInDialog, stopIgnoringUser} = this.props; + const {auth, auth: {user}, asset, showSignInDialog, stopIgnoringUser} = this.props; const {me} = this.props.root; const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0; @@ -63,14 +63,14 @@ class ProfileContainer extends Component { return ; } - const localProfile = this.props.user.profiles.find( + const localProfile = user.profiles.find( (p) => p.provider === 'local' ); const emailAddress = localProfile && localProfile.id; return (
-

{this.props.user.username}

+

{user.username}

{emailAddress ?

{emailAddress}

: null} {me.ignoredUsers && me.ignoredUsers.length @@ -138,9 +138,8 @@ const withProfileQuery = withQuery( `); const mapStateToProps = (state) => ({ - user: state.user.toJS(), - asset: state.asset.toJS(), - auth: state.auth.toJS() + asset: state.asset, + auth: state.auth }); const mapDispatchToProps = (dispatch) => diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 7f43cc5b2..22c30d639 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -363,7 +363,7 @@ export default (reaction) => (WrappedComponent) => { ); const mapStateToProps = (state) => ({ - user: state.auth.toJS().user, + user: state.auth.user, }); const mapDispatchToProps = (dispatch) => diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index e9b5bdff0..b0a74f4d7 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -84,7 +84,7 @@ export default (tag) => (WrappedComponent) => { } const mapStateToProps = (state) => ({ - user: state.auth.toJS().user, + user: state.auth.user, }); const mapDispatchToProps = (dispatch) => diff --git a/plugins/talk-plugin-auth/client/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/components/ChangeUsername.js index ed3c5ace0..c1ad081b1 100644 --- a/plugins/talk-plugin-auth/client/components/ChangeUsername.js +++ b/plugins/talk-plugin-auth/client/components/ChangeUsername.js @@ -122,7 +122,7 @@ class ChangeUsernameContainer extends React.Component { } const mapStateToProps = ({auth}) => ({ - auth: auth.toJS() + auth: auth }); const mapDispatchToProps = (dispatch) => diff --git a/plugins/talk-plugin-auth/client/components/SignInButton.js b/plugins/talk-plugin-auth/client/components/SignInButton.js index 0adb300a7..9c8a04b11 100644 --- a/plugins/talk-plugin-auth/client/components/SignInButton.js +++ b/plugins/talk-plugin-auth/client/components/SignInButton.js @@ -16,7 +16,7 @@ const SignInButton = ({loggedIn, showSignInDialog}) => ( ); const mapStateToProps = ({auth}) => ({ - loggedIn: auth.toJS().loggedIn + loggedIn: auth.loggedIn }); const mapDispatchToProps = (dispatch) => diff --git a/plugins/talk-plugin-auth/client/components/SignInContainer.js b/plugins/talk-plugin-auth/client/components/SignInContainer.js index 6950f4124..188651186 100644 --- a/plugins/talk-plugin-auth/client/components/SignInContainer.js +++ b/plugins/talk-plugin-auth/client/components/SignInContainer.js @@ -176,7 +176,7 @@ class SignInContainer extends React.Component { } const mapStateToProps = (state) => ({ - auth: state.auth.toJS() + auth: state.auth }); const mapDispatchToProps = (dispatch) => diff --git a/plugins/talk-plugin-auth/client/components/UserBox.js b/plugins/talk-plugin-auth/client/components/UserBox.js index 5b79b664a..61540d3c5 100644 --- a/plugins/talk-plugin-auth/client/components/UserBox.js +++ b/plugins/talk-plugin-auth/client/components/UserBox.js @@ -22,8 +22,8 @@ const UserBox = ({loggedIn, user, logout, onShowProfile}) => ( ); const mapStateToProps = ({auth}) => ({ - loggedIn: auth.toJS().loggedIn, - user: auth.toJS().user + loggedIn: auth.loggedIn, + user: auth.user }); const mapDispatchToProps = (dispatch) => From 2e8863285dc9f108222d2df66c1255246bb48712 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 14 Aug 2017 16:49:00 +0700 Subject: [PATCH 032/109] Rm immutable-js from admin --- client/coral-admin/src/actions/assets.js | 2 +- client/coral-admin/src/actions/install.js | 6 +- client/coral-admin/src/actions/settings.js | 2 +- client/coral-admin/src/containers/Layout.js | 6 +- .../coral-admin/src/containers/UserDetail.js | 4 +- client/coral-admin/src/reducers/assets.js | 51 ++++--- client/coral-admin/src/reducers/auth.js | 60 +++++--- client/coral-admin/src/reducers/community.js | 101 ++++++++------ client/coral-admin/src/reducers/config.js | 13 +- client/coral-admin/src/reducers/install.js | 131 +++++++++++------- client/coral-admin/src/reducers/moderation.js | 45 ++++-- client/coral-admin/src/reducers/settings.js | 81 ++++++----- .../routes/Community/containers/Community.js | 4 +- .../src/routes/Community/containers/Table.js | 2 +- .../routes/Configure/containers/Configure.js | 4 +- .../routes/Dashboard/containers/Dashboard.js | 4 +- .../src/routes/Install/containers/Install.js | 2 +- .../Moderation/containers/Moderation.js | 6 +- .../src/routes/Stories/containers/Stories.js | 2 +- .../client/containers/ModSubscription.js | 2 +- 20 files changed, 316 insertions(+), 212 deletions(-) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index 75bd2477d..b9ebe0ada 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -35,7 +35,7 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte // Update an asset state // Get comments to fill each of the three lists on the mod queue export const updateAssetState = (id, closedAt) => (dispatch) => { - dispatch({type: UPDATE_ASSET_STATE_REQUEST}); + dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt}); return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) .then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) .catch((error) => { diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index 33cefab63..6393c58dd 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -93,21 +93,21 @@ const validation = (formData, dispatch, next) => { }; export const submitSettings = () => (dispatch, getState) => { - const settingsFormData = getState().install.toJS().data.settings; + const settingsFormData = getState().install.data.settings; validation(settingsFormData, dispatch, function() { dispatch(nextStep()); }); }; export const submitUser = () => (dispatch, getState) => { - const userFormData = getState().install.toJS().data.user; + const userFormData = getState().install.data.user; validation(userFormData, dispatch, function() { dispatch(nextStep()); }); }; export const finishInstall = () => (dispatch, getState) => { - const data = getState().install.toJS().data; + const data = getState().install.data; dispatch(installRequest()); return coralApi('/setup', {method: 'POST', body: data}) .then(() => { diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index ba8b917ea..ca9062b9e 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -42,7 +42,7 @@ export const updateDomainlist = (listName, list) => { }; export const saveSettingsToServer = () => (dispatch, getState) => { - let settings = getState().settings.toJS(); + let settings = getState().settings; if (settings.charCount) { settings.charCount = parseInt(settings.charCount); } diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index 8b119fb72..ae022f2ef 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -74,10 +74,8 @@ class LayoutContainer extends Component { } const mapStateToProps = (state) => ({ - auth: state.auth.toJS(), - TALK_RECAPTCHA_PUBLIC: state.config - .get('data') - .get('TALK_RECAPTCHA_PUBLIC', null) + auth: state.auth, + TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index e59629a85..2ae6e4c9c 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -169,8 +169,8 @@ const mapStateToProps = (state) => ({ selectedCommentIds: state.userDetail.selectedCommentIds, statuses: state.userDetail.statuses, activeTab: state.userDetail.activeTab, - bannedWords: state.settings.toJS().wordlist.banned, - suspectWords: state.settings.toJS().wordlist.suspect, + bannedWords: state.settings.wordlist.banned, + suspectWords: state.settings.wordlist.suspect, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js index 03f0be9bb..bb8751973 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/assets.js @@ -1,35 +1,40 @@ -import {Map, List, fromJS} from 'immutable'; import * as actions from '../constants/assets'; +import update from 'immutability-helper'; -const initialState = Map({ - byId: Map(), - ids: List(), - assets: List() -}); +const initialState = { + byId: {}, + ids: [], + assets: [] +}; export default function assets (state = initialState, action) { switch (action.type) { - case actions.FETCH_ASSETS_SUCCESS: - return replaceAssets(action, state); + case actions.FETCH_ASSETS_SUCCESS: { + const assets = action.assets.reduce((prev, curr) => { + prev[curr.id] = curr; + return prev; + }, {}); + + return update(state, { + byId: {$set: assets}, + count: {$set: action.count}, + ids: {$set: Object.keys(assets)}, + }); + } case actions.UPDATE_ASSET_STATE_REQUEST: - return state - .setIn(['byId', action.id, 'closedAt'], action.closedAt); + return update(state, { + byId: { + [action.id]: { + closedAt: {$set: action.closedAt}, + }, + }, + }); case actions.UPDATE_ASSETS: - return state - .set('assets', List(action.assets)); + return update(state, { + assets: {$set: action.assets}, + }); default: return state; } } -const replaceAssets = (action, state) => { - const assets = fromJS(action.assets.reduce((prev, curr) => { - prev[curr.id] = curr; - return prev; - }, {})); - - return state - .set('byId', assets) - .set('count', action.count) - .set('ids', List(assets.keys())); -}; diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js index a60d73cec..a9c7f520e 100644 --- a/client/coral-admin/src/reducers/auth.js +++ b/client/coral-admin/src/reducers/auth.js @@ -1,43 +1,63 @@ -import {Map} from 'immutable'; import * as actions from '../constants/auth'; -const initialState = Map({ +const initialState = { loggedIn: false, user: null, loginError: null, loginMaxExceeded: false, passwordRequestSuccess: null -}); +}; export default function auth (state = initialState, action) { switch (action.type) { case actions.CHECK_LOGIN_REQUEST: - return state - .set('loadingUser', true); + return { + ...state, + loadingUser: true, + }; case actions.CHECK_LOGIN_FAILURE: - return state - .set('loggedIn', false) - .set('loadingUser', false) - .set('user', null); + return { + ...state, + loggedIn: false, + loadingUser: false, + user: null, + }; case actions.CHECK_LOGIN_SUCCESS: - return state - .set('loggedIn', true) - .set('loadingUser', false) - .set('user', action.user); + return { + ...state, + loggedIn: true, + loadingUser: false, + user: action.user, + }; case actions.LOGOUT: return initialState; case actions.LOGIN_SUCCESS: - return state.set('loginMaxExceeded', false).set('loginError', null); + return { + ...state, + loginMaxExceeded: false, + loginError: null, + }; case actions.LOGIN_FAILURE: - return state.set('loginError', action.message); + return { + ...state, + loginError: action.message, + }; case actions.FETCH_FORGOT_PASSWORD_REQUEST: - return state.set('passwordRequestSuccess', null); + return { + ...state, + passwordRequestSuccess: null, + }; case actions.FETCH_FORGOT_PASSWORD_SUCCESS: - return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.'); + return { + ...state, + passwordRequestSuccess: 'If you have a registered account, a password reset link was sent to that email.', + }; case actions.LOGIN_MAXIMUM_EXCEEDED: - return state - .set('loginMaxExceeded', true) - .set('loginError', action.message); + return { + ...state, + loginMaxExceeded: true, + loginError: action.message, + }; default : return state; } diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 3fba504f8..9f724c8d6 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -1,5 +1,3 @@ -import {Map} from 'immutable'; - import { FETCH_COMMENTERS_REQUEST, FETCH_COMMENTERS_FAILURE, @@ -13,8 +11,8 @@ import { HIDE_REJECT_USERNAME_DIALOG } from '../constants/community'; -const initialState = Map({ - community: Map(), +const initialState = { + community: {}, isFetchingPeople: false, errorPeople: '', accounts: [], @@ -22,72 +20,87 @@ const initialState = Map({ ascPeople: false, totalPagesPeople: 0, pagePeople: 0, - user: Map({}), + user: {}, banDialog: false, rejectUsernameDialog: false -}); +}; export default function community (state = initialState, action) { switch (action.type) { case FETCH_COMMENTERS_REQUEST : - return state - .set('isFetchingPeople', true); + return { + ...state, + isFetchingPeople: true, + }; case FETCH_COMMENTERS_FAILURE : - return state - .set('isFetchingPeople', false) - .set('errorPeople', action.error); - + return { + ...state, + isFetchingPeople: false, + errorPeople: action.error, + }; case FETCH_COMMENTERS_SUCCESS : { const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line - return state - .merge({ - isFetchingPeople: false, - errorPeople: '', - pagePeople: page, - countPeople: count, - limitPeople: limit, - totalPagesPeople: totalPages, - ...rest - }) - .set('accounts', accounts); // Sets to normal array + return { + ...state, + isFetchingPeople: false, + errorPeople: '', + pagePeople: page, + countPeople: count, + limitPeople: limit, + totalPagesPeople: totalPages, + ...rest, + accounts, // Sets to normal array + }; } case SET_ROLE : { - const commenters = state.get('accounts'); + const commenters = state.accounts; const idx = commenters.findIndex((el) => el.id === action.id); commenters[idx].roles[0] = action.role; - return state.set('accounts', commenters.map((id) => id)); + return { + ...state, + accounts: commenters.map((id) => id), + }; } case SET_COMMENTER_STATUS: { - const commenters = state.get('accounts'); + const commenters = state.accounts; const idx = commenters.findIndex((el) => el.id === action.id); commenters[idx].status = action.status; - return state.set('accounts', commenters.map((id) => id)); + return { + ...state, + accounts: commenters.map((id) => id), + }; } case SORT_UPDATE : - return state - .set('fieldPeople', action.sort.field) - .set('ascPeople', !state.get('ascPeople')); + return { + ...state, + fieldPeople: action.sort.field, + ascPeople: !state.ascPeople, + }; case HIDE_BANUSER_DIALOG: - return state - .set('banDialog', false); + return { + ...state, + banDialog: false, + }; case SHOW_BANUSER_DIALOG: - return state - .merge({ - user: Map(action.user), - banDialog: true - }); + return { + ...state, + user: action.user, + banDialog: true, + }; case HIDE_REJECT_USERNAME_DIALOG: - return state - .set('rejectUsernameDialog', false); + return { + ...state, + rejectUsernameDialog: false, + }; case SHOW_REJECT_USERNAME_DIALOG: - return state - .merge({ - user: Map(action.user), - rejectUsernameDialog: true - }); + return { + ...state, + user: action.user, + rejectUsernameDialog: true + }; default : return state; } diff --git a/client/coral-admin/src/reducers/config.js b/client/coral-admin/src/reducers/config.js index a5d11d6eb..310a198ed 100644 --- a/client/coral-admin/src/reducers/config.js +++ b/client/coral-admin/src/reducers/config.js @@ -1,15 +1,16 @@ -import {Map} from 'immutable'; - import * as actions from '../actions/config'; -const initialState = Map({ - data: Map({}) -}); +const initialState = { + data: {} +}; export default function config (state = initialState, action) { switch (action.type) { case actions.CONFIG_UPDATED: - return state.set('data', Map(action.data)); + return { + ...state, + data: action.data, + }; default: return state; } diff --git a/client/coral-admin/src/reducers/install.js b/client/coral-admin/src/reducers/install.js index 1f0f079ff..c6e85c69f 100644 --- a/client/coral-admin/src/reducers/install.js +++ b/client/coral-admin/src/reducers/install.js @@ -1,30 +1,29 @@ -import {Map, List} from 'immutable'; - import * as actions from '../constants/install'; +import update from 'immutability-helper'; -const initialState = Map({ +const initialState = { isLoading: false, - data: Map({ - settings: Map({ + data: { + settings: { organizationName: '', - domains: Map({ - whitelist: List() - }) - }), - user: Map({ + domains: { + whitelist: [], + } + }, + user: { username: '', email: '', password: '', confirmPassword: '' - }) - }), - errors: Map({ + } + }, + errors: { organizationName: '', username: '', email: '', password: '', confirmPassword: '' - }), + }, showErrors: false, hasError: false, error: null, @@ -44,57 +43,91 @@ const initialState = Map({ installRequest: null, installRequestError: null, alreadyInstalled: false -}); +}; export default function install (state = initialState, action) { switch (action.type) { case actions.NEXT_STEP: - return state - .set('step', state.get('step') + 1); + return { + ...state, + step: state.step + 1, + }; case actions.PREVIOUS_STEP: - return state - .set('step', state.get('step') - 1); + return { + ...state, + step: state.step - 1, + }; case actions.GO_TO_STEP: - return state - .set('step', action.step); + return { + ...state, + step: action.step, + }; case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS: - return state - .setIn(['data', 'settings', 'domains', 'whitelist'], action.value); + return update(state, { + data: { + settings: { + domains: { + whitelist: {$set: action.value}, + }, + }, + }, + }); case actions.UPDATE_FORMDATA_SETTINGS: - return state - .setIn(['data', 'settings', action.name], action.value); + return update(state, { + data: { + settings: { + [action.name]: {$set: action.value}, + }, + }, + }); case actions.UPDATE_FORMDATA_USER: - return state - .setIn(['data', 'user', action.name], action.value); + return update(state, { + data: { + user: { + [action.name]: {$set: action.value}, + }, + }, + }); case actions.HAS_ERROR: - return state - .merge({ - hasError: true, - showErrors: true - }); + return { + ...state, + hasError: true, + showErrors: true, + }; case actions.ADD_ERROR: - return state - .setIn(['errors', action.name], action.error); + return update(state, { + errors: { + [action.name]: {$set: action.error}, + }, + }); case actions.CLEAR_ERRORS: - return state - .set('errors', Map()); + return { + ...state, + errors: {}, + }; case actions.INSTALL_REQUEST: - return state - .set('isLoading', true); + return { + ...state, + isLoading: true, + }; case actions.INSTALL_SUCCESS: - return state - .set('isLoading', false) - .set('installRequest', 'SUCCESS'); + return { + ...state, + isLoading: false, + installRequest: 'SUCCESS', + }; case actions.INSTALL_FAILURE: - return state - .merge({ - isLoading: false, - installRequest: 'FAILURE', - installRequestError: action.error - }); + return { + ...state, + isLoading: false, + installRequest: 'FAILURE', + installRequestError: action.error + }; case actions.CHECK_INSTALL_SUCCESS: - return state - .set('alreadyInstalled', action.installed); + return { + ...state, + alreadyInstalled: action.installed, + }; default : return state; } diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index a0cba1f5f..e0a608488 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -1,37 +1,54 @@ -import {fromJS} from 'immutable'; import * as actions from '../constants/moderation'; -const initialState = fromJS({ +const initialState = { singleView: false, modalOpen: false, storySearchVisible: false, storySearchString: '', shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show', sortOrder: 'REVERSE_CHRONOLOGICAL', -}); +}; export default function moderation (state = initialState, action) { switch (action.type) { case actions.MODERATION_CLEAR_STATE: return initialState; case actions.TOGGLE_MODAL: - return state - .set('modalOpen', action.open); + return { + ...state, + modalOpen: action.open, + }; case actions.SINGLE_VIEW: - return state - .set('singleView', !state.get('singleView')); + return { + ...state, + singleView: !state.singleView, + }; case actions.HIDE_SHORTCUTS_NOTE: - return state - .set('shortcutsNoteVisible', 'hide'); + return { + ...state, + shortcutsNoteVisible: 'hide', + }; case actions.SHOW_STORY_SEARCH: - return state.set('storySearchVisible', true); + return { + ...state, + storySearchVisible: true, + }; case actions.HIDE_STORY_SEARCH: - return state.set('storySearchVisible', false); + return { + ...state, + storySearchVisible: false, + }; case actions.STORY_SEARCH_CHANGE_VALUE: - return state.set('storySearchString', action.value); + return { + ...state, + storySearchString: action.value, + }; case actions.SET_SORT_ORDER: - return state.set('sortOrder', action.order); - default : + return { + ...state, + sortOrder: action.order, + }; + default: return state; } } diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js index 67907b327..6e29ba719 100644 --- a/client/coral-admin/src/reducers/settings.js +++ b/client/coral-admin/src/reducers/settings.js @@ -1,5 +1,5 @@ -import {Map, List} from 'immutable'; import * as actions from '../actions/settings'; +import update from 'immutability-helper'; // this is initialized here because // currently you have to reload the dashboard to get new stats @@ -10,63 +10,80 @@ const DASHBOARD_WINDOW_MINUTES = 5; let then = new Date(); then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES); -const initialState = Map({ - wordlist: Map({ - banned: List(), - suspect: List() - }), +const initialState = { + wordlist: { + banned: [], + suspect: [] + }, dashboardWindowStart: then.toISOString(), dashboardWindowEnd: new Date().toISOString(), - domains: Map({ - whitelist: List() - }), + domains: { + whitelist: [] + }, saveSettingsError: null, fetchSettingsError: null, fetchingSettings: false -}); +}; export default function settings (state = initialState, action) { switch (action.type) { case actions.SETTINGS_LOADING: - return state - .set('fetchingSettings', true) - .set('fetchSettingsError', null); + return { + ...state, + fetchingSettings: true, + fetchSettingsError: null, + }; case actions.SETTINGS_RECEIVED: - return state.merge({ + return { + ...state, fetchingSettings: false, fetchSettingsError: null, ...action.settings - }); + }; case actions.SETTINGS_FETCH_ERROR: - return state - .set('fetchingSettings', false) - .set('fetchSettingsError', action.error); + return { + ...state, + fetchingSettings: false, + fetchSettingsError: action.error, + }; case actions.SETTINGS_UPDATED: - return state.merge({ + return { + ...state, fetchingSettings: false, fetchSettingsError: null, ...action.settings - }); + }; case actions.SAVE_SETTINGS_LOADING: - return state - .set('fetchingSettings', true) - .set('saveSettingsError', null); + return { + ...state, + fetchingSettings: true, + saveSettingsError: null, + }; case actions.SAVE_SETTINGS_SUCCESS: - return state.merge({ + return { + ...state, fetchingSettings: false, fetchSettingsError: null, ...action.settings - }); + }; case actions.SAVE_SETTINGS_FAILED: - return state - .set('fetchingSettings', false) - .set('fetchSettingsError', action.error); + return { + ...state, + fetchingSettings: false, + fetchSettingsError: action.error, + }; case actions.WORDLIST_UPDATED: - return state - .setIn(['wordlist', action.listName], action.list); + return update(state, { + wordList: { + [action.listName]: {$set: action.list}, + } + }); case actions.DOMAINLIST_UPDATED: - return state - .setIn(['domains', action.listName], action.list); + return update(state, { + domains: { + [action.listName]: {$set: action.list}, + } + }); default: return state; } diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index a9a164ac9..b04fc0281 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -87,8 +87,8 @@ export const withCommunityQuery = withQuery(gql` }); const mapStateToProps = (state) => ({ - community: state.community.toJS(), - currentUser: state.auth.toJS().user, + community: state.community, + currentUser: state.auth.user, }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-admin/src/routes/Community/containers/Table.js b/client/coral-admin/src/routes/Community/containers/Table.js index 8bd5395e4..5d4e4914f 100644 --- a/client/coral-admin/src/routes/Community/containers/Table.js +++ b/client/coral-admin/src/routes/Community/containers/Table.js @@ -23,7 +23,7 @@ class TableContainer extends Component { } const mapStateToProps = (state) => ({ - commenters: state.community.get('accounts'), + commenters: state.community.accounts, }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index c68b39e0c..aff1e57b2 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -23,8 +23,8 @@ class ConfigureContainer extends Component { } const mapStateToProps = (state) => ({ - auth: state.auth.toJS(), - settings: state.settings.toJS() + auth: state.auth, + settings: state.settings }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js index ea1fb2b6c..74faedcf7 100644 --- a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js +++ b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js @@ -54,8 +54,8 @@ export const witDashboardQuery = withQuery(gql` const mapStateToProps = (state) => { return { - settings: state.settings.toJS(), - moderation: state.moderation.toJS() + settings: state.settings, + moderation: state.moderation }; }; diff --git a/client/coral-admin/src/routes/Install/containers/Install.js b/client/coral-admin/src/routes/Install/containers/Install.js index 5dcb35705..442127e47 100644 --- a/client/coral-admin/src/routes/Install/containers/Install.js +++ b/client/coral-admin/src/routes/Install/containers/Install.js @@ -35,7 +35,7 @@ InstallContainer.contextTypes = { }; const mapStateToProps = (state) => ({ - install: state.install.toJS() + install: state.install }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index d7f62491d..d5ca5c840 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -376,9 +376,9 @@ const withQueueCountPolling = withQuery(gql` }); const mapStateToProps = (state) => ({ - moderation: state.moderation.toJS(), - settings: state.settings.toJS(), - auth: state.auth.toJS(), + moderation: state.moderation, + settings: state.settings, + auth: state.auth, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/client/coral-admin/src/routes/Stories/containers/Stories.js b/client/coral-admin/src/routes/Stories/containers/Stories.js index 450efcf74..f645f310c 100644 --- a/client/coral-admin/src/routes/Stories/containers/Stories.js +++ b/client/coral-admin/src/routes/Stories/containers/Stories.js @@ -12,7 +12,7 @@ class StoriesContainer extends Component { } const mapStateToProps = (state) => ({ - assets: state.assets.toJS() + assets: state.assets }); const mapDispatchToProps = (dispatch) => diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js index 85e0d8fe2..82f323530 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js @@ -91,7 +91,7 @@ const COMMENT_UNFEATURED_SUBSCRIPTION = gql` `; const mapStateToProps = (state) => ({ - user: state.auth.toJS().user, + user: state.auth.user, }); export default connect(mapStateToProps, null)(ModSubscription); From cdfef8c9896da81538fe321713c2bd1ee21b63cc Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 14 Aug 2017 16:56:51 +0700 Subject: [PATCH 033/109] Remove immutable --- package.json | 1 - yarn.lock | 17 +++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index bb0a724ac..a0a1efe17 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,6 @@ "hammerjs": "^2.0.8", "history": "^3.0.0", "ignore-styles": "^5.0.1", - "immutable": "^3.8.1", "imports-loader": "^0.7.1", "istanbul": "^1.1.0-alpha.1", "jsdom": "^9.8.3", diff --git a/yarn.lock b/yarn.lock index a5d73a756..9244af84f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3996,11 +3996,11 @@ iconv-lite@0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" -iconv-lite@0.4.15, iconv-lite@^0.4.5, iconv-lite@~0.4.13: +iconv-lite@0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" -iconv-lite@^0.4.17: +iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13: version "0.4.18" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" @@ -4034,10 +4034,6 @@ immutability-helper@^2.2.0: dependencies: invariant "^2.2.0" -immutable@^3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" - imports-loader@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.7.1.tgz#f204b5f34702a32c1db7d48d89d5e867a0441253" @@ -7869,14 +7865,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" - -string-width@^2.1.0: +string-width@^2.0.0, string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: From 06cf33521fb26ae4e6ba9b6fdf5eb2a858161b8f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 14 Aug 2017 19:40:52 +0700 Subject: [PATCH 034/109] Remove immutable-js traces from docs --- docs/_docs/03-05-client-architecture.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/_docs/03-05-client-architecture.md b/docs/_docs/03-05-client-architecture.md index 6bb44bfad..6996811c5 100644 --- a/docs/_docs/03-05-client-architecture.md +++ b/docs/_docs/03-05-client-architecture.md @@ -6,7 +6,6 @@ permalink: /docs/architecture/client ## The Stack - [React](#react) - [Redux](#redux) - - [ImmutableJS](#immutablejs) ## The Architecture @@ -96,14 +95,6 @@ We use [Apollo](http://www.apollodata.com/) to handle graph requests and handle ## Redux We use [Redux](http://redux.js.org/) to handle the auth state. - -## ImmutableJS -We use Immutable JS to maintain our state immutable. -We found some really good tradeoffs while building Talk. - -[How to use ImmutableJS and how we use it with Talk](https://facebook.github.io/immutable-js/docs/#/) - - ## Test [How we do testing at Coral with Talk]({{ "/docs/development/tools" | absolute_url }}) From abda77a7e725fe4fa2a304527d51ec21994a7fb3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 14 Aug 2017 19:55:04 +0700 Subject: [PATCH 035/109] Fix console error in offtopic --- .../talk-plugin-offtopic/client/components/OffTopicFilter.js | 1 - .../talk-plugin-offtopic/client/containers/OffTopicFilter.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js index 171bafeb2..426dea29a 100644 --- a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js @@ -16,7 +16,6 @@ export default class OffTopicFilter extends React.Component { this.props.removeCommentClassName(idx); this.props.toggleCheckbox(); } - this.props.closeViewingOptions(); } render() { diff --git a/plugins/talk-plugin-offtopic/client/containers/OffTopicFilter.js b/plugins/talk-plugin-offtopic/client/containers/OffTopicFilter.js index 9548b764e..7eab8c526 100644 --- a/plugins/talk-plugin-offtopic/client/containers/OffTopicFilter.js +++ b/plugins/talk-plugin-offtopic/client/containers/OffTopicFilter.js @@ -3,9 +3,6 @@ import {bindActionCreators} from 'redux'; import {toggleCheckbox} from '../actions'; import {commentClassNamesSelector} from 'plugin-api/alpha/client/selectors'; import OffTopicFilter from '../components/OffTopicFilter'; -import { - closeViewingOptions -} from 'plugins/talk-plugin-viewing-options/client/actions'; import { addCommentClassName, removeCommentClassName @@ -20,7 +17,6 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( { toggleCheckbox, - closeViewingOptions, addCommentClassName, removeCommentClassName }, From c52a97338c7ad05e1ea115436bf0d39624619842 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 14 Aug 2017 21:25:48 +0700 Subject: [PATCH 036/109] Fix FakeComment styles --- .../client/components/CreateUsernameDialog.js | 2 +- .../client/components/FakeComment.css | 37 ++++++++++ .../client/components/FakeComment.js | 72 +++++++------------ 3 files changed, 63 insertions(+), 48 deletions(-) create mode 100644 plugins/talk-plugin-auth/client/components/FakeComment.css diff --git a/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js b/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js index 12916b4dd..4b0babed6 100644 --- a/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js +++ b/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js @@ -32,7 +32,7 @@ const CreateUsernameDialog = ({ className={styles.fakeComment} username={formData.username} created_at={Date.now()} - comment={{body:t('createdisplay.fake_comment_body')}} + body={t('createdisplay.fake_comment_body')} />

{t('createdisplay.if_you_dont_change_your_name')} diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.css b/plugins/talk-plugin-auth/client/components/FakeComment.css new file mode 100644 index 000000000..8b5521aea --- /dev/null +++ b/plugins/talk-plugin-auth/client/components/FakeComment.css @@ -0,0 +1,37 @@ +.root { + border-top: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + margin-bottom: 10px; + padding: 8px 0px 10px 0px; + position: relative; +} + +.body { + +} + +.footer { + display: flex; + justify-content: space-between; +} + +.button { + color: #2a2a2a; + margin: 5px 10px 5px 0px; + background: none; + padding: 0px; + border: none; + font-size: inherit; + vertical-align: middle; + + &:hover { + color: #767676; + cursor: pointer; + } +} + +.icon { + font-size: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; +} diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.js b/plugins/talk-plugin-auth/client/components/FakeComment.js index b6b4e7609..eefada587 100644 --- a/plugins/talk-plugin-auth/client/components/FakeComment.js +++ b/plugins/talk-plugin-auth/client/components/FakeComment.js @@ -2,65 +2,43 @@ import React from 'react'; import t from 'coral-framework/services/i18n'; import {ReplyButton} from 'talk-plugin-replies'; import PubDate from 'talk-plugin-pubdate/PubDate'; -import Slot from 'coral-framework/components/Slot'; import AuthorName from 'talk-plugin-author-name/AuthorName'; -import styles from 'coral-embed-stream/src/components/Comment.css'; +import styles from './FakeComment.css'; +import {Icon} from 'plugin-api/beta/client/components/ui'; -export const FakeComment = ({username, created_at, comment}) => ( -

-
- +export const FakeComment = ({username, created_at, body}) => ( +
+ - -
-
- + {}} + parentCommentId={'commentID'} + currentUserId={{}} + />
- {}} - parentCommentId={'commentID'} - currentUserId={{}} - /> -
-
-
- -
-
-
From db37a0b38ef3226ee34be491eb2622e65b0646b0 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 15 Aug 2017 16:43:44 +0700 Subject: [PATCH 037/109] Check existance of featuredComments in query --- plugins/talk-plugin-featured-comments/client/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js index 82cba8b91..ad10f87ca 100644 --- a/plugins/talk-plugin-featured-comments/client/index.js +++ b/plugins/talk-plugin-featured-comments/client/index.js @@ -26,6 +26,9 @@ export default { IgnoreUser: ({variables}) => ({ updateQueries: { CoralEmbedStream_Embed: (previous) => { + if (!previous.asset.featuredComments) { + return previous; + } const ignoredUserId = variables.id; const newNodes = previous.asset.featuredComments.nodes.filter((n) => n.user.id !== ignoredUserId); const removedCount = previous.asset.featuredComments.nodes.length - newNodes.length; From b63fce4ebc9ae7dc001f05d6e557e206a181a199 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 15 Aug 2017 16:58:23 +0700 Subject: [PATCH 038/109] Optimize Slot rendering --- client/coral-framework/components/Slot.js | 49 ++++++++++++++++++----- client/coral-framework/helpers/plugins.js | 6 +++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index b1df6647a..f86d44eb1 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -4,19 +4,48 @@ import styles from './Slot.css'; import {connect} from 'react-redux'; import {getSlotElements} from 'coral-framework/helpers/plugins'; import omit from 'lodash/omit'; +import union from 'lodash/union'; +import isEqual from 'lodash/isEqual'; -function Slot ({fill, inline = false, className, reduxState, defaultComponent: DefaultComponent, ...rest}) { - let children = getSlotElements(fill, reduxState, rest); - const pluginConfig = reduxState.config.pluginConfig || {}; - if (children.length === 0 && DefaultComponent) { - children = ; +class Slot extends React.Component { + shouldComponentUpdate(next) { + + // Prevent Slot from rerendering when only reduxState has changed and + // it does not result in a change of slot children. + const keys = union(Object.keys(this.props), Object.keys(next)); + const changes = keys.filter((key) => this.props[key] !== next[key]); + if (changes.length === 1 && changes[0] === 'reduxState') { + const prevChildrenUuid = this.getChildren(this.props).map((child) => child.type.talkUuid); + const nextChildrenUuid = this.getChildren(next).map((child) => child.type.talkUuid); + return !isEqual(prevChildrenUuid, nextChildrenUuid); + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; } - return ( -
- {children} -
- ); + getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, ...rest} = this.props) { + return rest; + } + + getChildren(props = this.props) { + return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props)); + } + + render() { + const {inline = false, className, reduxState, defaultComponent: DefaultComponent} = this.props; + let children = this.getChildren(); + const pluginConfig = reduxState.config.pluginConfig || {}; + if (children.length === 0 && DefaultComponent) { + children = ; + } + + return ( +
+ {children} +
+ ); + } } Slot.propTypes = { diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index b2ca78142..7bebaa1f6 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -9,6 +9,7 @@ import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers} from 'coral-framework/services/store'; import camelize from './camelize'; import plugins from 'pluginsConfig'; +import uuid from 'uuid/v4'; export function getSlotComponents(slot, reduxState, props = {}) { const pluginConfig = reduxState.config.plugin_config || {}; @@ -100,7 +101,12 @@ function addMetaDataToSlotComponents() { const slots = plugin.module.slots; slots && Object.keys(slots).forEach((slot) => { slots[slot].forEach((component) => { + + // Attach plugin name to the component component.talkPluginName = plugin.name; + + // Attach uuid to the component + component.talkUuid = uuid(); }); }); }); From 4cfdb6574bfc651a8cf829ee387c59ec7597dc60 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 15 Aug 2017 21:23:39 +0700 Subject: [PATCH 039/109] withQuery props should be immutable --- client/coral-framework/hocs/withQuery.js | 129 ++++++++++++++--------- 1 file changed, 82 insertions(+), 47 deletions(-) diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index adb67b7b3..2ddca7c7e 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -46,6 +46,7 @@ export default (document, config = {}) => (WrappedComponent) => { // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; lastNetworkStatus = null; + data = null; emitWhenNeeded(data) { const {variables, networkStatus} = data; @@ -60,59 +61,93 @@ export default (document, config = {}) => (WrappedComponent) => { this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, data: root}); } + nextData(data) { + this.emitWhenNeeded(data); + + // If data was previously set, we update it in a immutable way. + if (this.data) { + if (this.data.networkStatus !== data.networkStatus || + this.data.loading !== data.loading || + this.data.error !== data.error || + this.data.variables !== data.variables) { + this.data = { + ...this.data, + error: data.error, + networkStatus: data.networkStatus, + loading: data.loading, + variables: data.variables, + }; + } + } + else { + + // Set data for the first time. + this.data = { + error: data.error, + variables: data.variables, + networkStatus: data.networkStatus, + loading: data.loading, + startPolling: data.startPolling, + stopPolling: data.stopPolling, + refetch: data.refetch, + updateQuery: data.updateQuery, + subscribeToMore: (stmArgs) => { + + // Resolve document fragments before passing it to `apollo-client`. + return data.subscribeToMore({ + ...stmArgs, + document: resolveFragments(stmArgs.document), + onError: (err) => { + if (stmArgs.onErr) { + return stmArgs.onErr(err); + } + throw err; + }, + }); + }, + fetchMore: (lmArgs) => { + const fetchName = getDefinitionName(lmArgs.query); + this.context.eventEmitter.emit( + `query.${name}.fetchMore.${fetchName}.begin`, + {variables: lmArgs.variables}); + + // Resolve document fragments before passing it to `apollo-client`. + return data.fetchMore({ + ...lmArgs, + query: resolveFragments(lmArgs.query), + }) + .then((res) => { + this.context.eventEmitter.emit( + `query.${name}.fetchMore.${fetchName}.success`, + {variables: lmArgs.variables, data: res.data}); + return Promise.resolve(res); + }) + .catch((err) => { + this.context.eventEmitter.emit( + `query.${name}.fetchMore.${fetchName}.error`, + {variables: lmArgs.variables, error: err}); + throw err; + }); + }, + }; + } + return this.data; + } + wrappedConfig = { ...config, options: config.options || {}, props: (args) => { - this.emitWhenNeeded(args.data); + const nextData = this.nextData(args.data); + const {root} = separateDataAndRoot(args.data); + if (config.props) { - const wrappedArgs = { - ...args, - data: { - ...args.data, - subscribeToMore: (stmArgs) => { + // Custom props, in this case we just pass the wrapped args to it. + return config.props({...args, data: {...args.data, ...nextData}}); + } - // Resolve document fragments before passing it to `apollo-client`. - return args.data.subscribeToMore({ - ...stmArgs, - document: resolveFragments(stmArgs.document), - onError: (err) => { - if (stmArgs.onErr) { - return stmArgs.onErr(err); - } - throw err; - }, - }); - }, - fetchMore: (lmArgs) => { - const fetchName = getDefinitionName(lmArgs.query); - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.begin`, - {variables: lmArgs.variables}); - - // Resolve document fragments before passing it to `apollo-client`. - return args.data.fetchMore({ - ...lmArgs, - query: resolveFragments(lmArgs.query), - }) - .then((res) => { - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.success`, - {variables: lmArgs.variables, data: res.data}); - return Promise.resolve(res); - }) - .catch((err) => { - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.error`, - {variables: lmArgs.variables, error: err}); - throw err; - }); - }, - }, - }; - return config.props - ? config.props(wrappedArgs) - : separateDataAndRoot(wrappedArgs.data); + // Return our wrapped data with a separated root. + return {...args, data: nextData, root}; }, }; From 2b2e2f9b5f5fef5a67f6289e9ae2d4f7189933ce Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 15 Aug 2017 21:32:44 +0700 Subject: [PATCH 040/109] ClickOutside should only toggle action when open --- client/coral-embed-stream/src/components/Toggleable.js | 4 +++- client/talk-plugin-flags/components/FlagButton.js | 4 +++- .../client/components/PermalinkButton.js | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/components/Toggleable.js b/client/coral-embed-stream/src/components/Toggleable.js index 49f2a701b..a65e8212c 100644 --- a/client/coral-embed-stream/src/components/Toggleable.js +++ b/client/coral-embed-stream/src/components/Toggleable.js @@ -19,7 +19,9 @@ export default class Toggleable extends React.Component { } close = () => { - this.setState({isOpen: false}); + if (this.state.isOpen) { + this.setState({isOpen: false}); + } } render() { diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index 85ceed0ea..1d8438ba0 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -133,7 +133,9 @@ export default class FlagButton extends Component { } handleClickOutside = () => { - this.closeMenu(); + if (this.state.showMenu) { + this.closeMenu(); + } } render () { diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js index 27cbf9188..e2536f86f 100644 --- a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js +++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js @@ -28,9 +28,11 @@ export default class PermalinkButton extends React.Component { } handleClickOutside = () => { - this.setState({ - popoverOpen: false - }); + if (this.state.popoverOpen) { + this.setState({ + popoverOpen: false + }); + } } copyPermalink = () => { From 33aea66ff774bf5ff48938eb4851018765e04050 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 15 Aug 2017 21:56:22 +0700 Subject: [PATCH 041/109] Refactor --- client/coral-framework/components/Slot.js | 7 +++---- client/coral-framework/utils/index.js | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index f86d44eb1..0b0f4d325 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -4,16 +4,15 @@ import styles from './Slot.css'; import {connect} from 'react-redux'; import {getSlotElements} from 'coral-framework/helpers/plugins'; import omit from 'lodash/omit'; -import union from 'lodash/union'; import isEqual from 'lodash/isEqual'; +import {getShallowChanges} from 'coral-framework/utils'; class Slot extends React.Component { shouldComponentUpdate(next) { // Prevent Slot from rerendering when only reduxState has changed and // it does not result in a change of slot children. - const keys = union(Object.keys(this.props), Object.keys(next)); - const changes = keys.filter((key) => this.props[key] !== next[key]); + const changes = getShallowChanges(this.props, next); if (changes.length === 1 && changes[0] === 'reduxState') { const prevChildrenUuid = this.getChildren(this.props).map((child) => child.type.talkUuid); const nextChildrenUuid = this.getChildren(next).map((child) => child.type.talkUuid); @@ -49,7 +48,7 @@ class Slot extends React.Component { } Slot.propTypes = { - fill: React.PropTypes.string + fill: React.PropTypes.string.isRequired }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 63dfd7dc4..2b4b6378f 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -1,5 +1,6 @@ import {gql} from 'react-apollo'; import t from 'coral-framework/services/i18n'; +import union from 'lodash/union'; import {capitalize} from 'coral-framework/helpers/strings'; export const getTotalActionCount = (type, comment) => { @@ -184,3 +185,8 @@ export function getSlotFragmentSpreads(slots, resource) { export function isCommentActive(commentStatus) { return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0; } + +export function getShallowChanges(a, b) { + return union(Object.keys(a), Object.keys(b)) + .filter((key) => a[key] !== b[key]); +} From e71eb28e4234e7dedb27a7fdf890a724070ec6e1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 15 Aug 2017 14:19:58 -0600 Subject: [PATCH 042/109] added base path support for router, spelling fixes --- app.js | 192 ++++------------------ config.js | 12 +- docs/_docs/00-01-faq.md | 69 ++++++-- docs/_docs/01-02-install-docker.md | 5 +- docs/_docs/01-03-install-source.md | 2 +- docs/_docs/01-05-install-microservices.md | 2 +- docs/_docs/02-01-configuration.md | 19 ++- docs/_docs/02-02-secrets.md | 2 +- docs/_docs/03-05-client-architecture.md | 2 +- docs/_docs/04-01-plugins.md | 2 +- docs/_docs/04-02-plugins-quickstart.md | 2 +- docs/_docs/04-03-plugins-client.md | 2 +- docs/_docs/04-04-plugins-server.md | 6 +- middleware/pubsub.js | 13 ++ routes/index.js | 142 +++++++++++++++- 15 files changed, 275 insertions(+), 197 deletions(-) create mode 100644 middleware/pubsub.js diff --git a/app.js b/app.js index 0be7a1830..9abb921ae 100644 --- a/app.js +++ b/app.js @@ -3,71 +3,43 @@ const bodyParser = require('body-parser'); const morgan = require('morgan'); const path = require('path'); const helmet = require('helmet'); -const authentication = require('./middleware/authentication'); -const {passport} = require('./services/passport'); -const plugins = require('./services/plugins'); -const pubsub = require('./services/pubsub'); -const i18n = require('./services/i18n'); -const enabled = require('debug').enabled; -const errors = require('./errors'); -const {createGraphOptions} = require('./graph'); -const apollo = require('graphql-server-express'); -const accepts = require('accepts'); const compression = require('compression'); const cookieParser = require('cookie-parser'); -const {ROOT_URL} = require('./config'); +const {ROOT_URL, ROOT_URL_MOUNT_PATH} = require('./config'); +const routes = require('./routes'); +const debug = require('debug')('talk:app'); +const {URL} = require('url'); const app = express(); -// Middleware declarations. +//============================================================================== +// APPLICATION WIDE MIDDLEWARE +//============================================================================== // Add the logging middleware only if we aren't testing. -if (app.get('env') !== 'test') { +if (process.env.NODE_ENV !== 'test') { app.use(morgan('dev')); } -//============================================================================== -// APP MIDDLEWARE -//============================================================================== - +// Trust the first proxy in front of us, this will enable us to trust the fact +// that SSL was terminated correctly. app.set('trust proxy', 1); -// We disable frameward on helmet to allow crossdomain injection of the embed +// Enable a suite of security good practices through helmet. We disable +// frameguard to allow crossdomain injection of the embed. app.use(helmet({ - frameguard: false + frameguard: false, })); + +// Compress the responses if appropriate. app.use(compression()); + +// Parse the cookies on the request. app.use(cookieParser()); + +// Parse the body json if it's there. app.use(bodyParser.json()); -//============================================================================== -// STATIC FILES -//============================================================================== - -// If the application is in production mode, then add gzip rewriting for the -// content. -if (process.env.NODE_ENV === 'production') { - app.get('*.js', (req, res, next) => { - const accept = accepts(req); - if (accept.encoding(['gzip']) === 'gzip') { - - // Adjsut the headers on the request by adding a content type header - // because express won't be able to detect the mime-type with the .gz - // extension and we need to decalre support for the gzip encoding. - res.set('Content-Type', 'application/javascript'); - res.set('Content-Encoding', 'gzip'); - - // Rewrite the url so that the gzip version will be served instead. - req.url = `${req.url}.gz`; - } - - next(); - }); -} - -app.use('/client', express.static(path.join(__dirname, 'dist'))); -app.use('/public', express.static(path.join(__dirname, 'public'))); - //============================================================================== // VIEW CONFIGURATION //============================================================================== @@ -75,124 +47,30 @@ app.use('/public', express.static(path.join(__dirname, 'public'))); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); -// Set the BASE_URL as the ROOT_URL. -app.locals.BASE_URL = ROOT_URL; -if (app.locals.BASE_URL[app.locals.BASE_URL.length - 1] !== '/') { - app.locals.BASE_URL += '/'; -} - -//============================================================================== -// PASSPORT MIDDLEWARE -//============================================================================== - -const passportDebug = require('debug')('talk:passport'); - -// Install the passport plugins. -plugins.get('server', 'passport').forEach((plugin) => { - passportDebug(`added plugin '${plugin.plugin.name}'`); - - // Pass the passport.js instance to the plugin to allow it to inject it's - // functionality. - plugin.passport(passport); -}); - -// Setup the PassportJS Middleware. -app.use(passport.initialize()); - -// Attach the authentication middleware, this will be responsible for decoding -// (if present) the JWT on the request. -app.use('/api', authentication); - -const pubsubClient = pubsub.createClientFactory(); - -// To handle dependancy injection safer, we inject the pubsub handle onto the -// request object. -app.use('/api', (req, res, next) => { - - // Attach the pubsub handle to the requests. - req.pubsub = pubsubClient(); - - // Forward on the request. - next(); -}); - -//============================================================================== -// GraphQL Router -//============================================================================== - -// GraphQL endpoint. -app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions)); - -// Only include the graphiql tool if we aren't in production mode. -if (app.get('env') !== 'production') { - - // Interactive graphiql interface. - app.use('/api/v1/graph/iql', (req, res) => { - res.render('graphiql', { - endpointURL: '/api/v1/graph/ql' - }); - }); - - // GraphQL documention. - app.get('/admin/docs', (req, res) => { - res.render('admin/docs'); - }); - -} - //============================================================================== // ROUTES //============================================================================== -app.use('/', require('./routes')); +// Set the BASE_URL as the ROOT_URL, here we derive the root url by ensuring +// that it ends in a `/`. +const BASE_URL = ROOT_URL && ROOT_URL.length > 0 && ROOT_URL[ROOT_URL.length - 1] === '/' ? ROOT_URL : `${ROOT_URL}/`; -//============================================================================== -// ERROR HANDLING -//============================================================================== +// The BASE_PATH is simply the path component of the BASE_URL. +const BASE_PATH = new URL(BASE_URL).pathname; -// Catch 404 and forward to error handler. -app.use((req, res, next) => { - next(errors.ErrNotFound); -}); +// The MOUNT_PATH is derived from the BASE_PATH, if it is provided and enabled. +// This will mount all the application routes onto it. +const MOUNT_PATH = ROOT_URL_MOUNT_PATH ? BASE_PATH : '/'; -// General error handler. Respond with the message and error if we have it while -// returning a status code that makes sense. -app.use('/api', (err, req, res, next) => { - if (err !== errors.ErrNotFound) { - if (app.get('env') !== 'test' || enabled('talk:errors')) { - console.error(err); - } - } +// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will +// make them available on the templates and the routers. +app.locals.BASE_URL = BASE_URL; +app.locals.BASE_PATH = BASE_PATH; +app.locals.MOUNT_PATH = MOUNT_PATH; - if (err instanceof errors.APIError) { - res.status(err.status).json({ - message: err.message, - error: err - }); - } else { - res.status(500).json({}); - } -}); +debug(`mounting routes on the ${MOUNT_PATH} path`); -app.use('/', (err, req, res, next) => { - if (err !== errors.ErrNotFound) { - console.error(err); - } - - i18n.init(req); - - if (err instanceof errors.APIError) { - res.status(err.status); - res.render('error', { - message: err.message, - error: app.get('env') === 'development' ? err : {} - }); - } else { - res.render('error', { - message: err.message, - error: app.get('env') === 'development' ? err : {} - }); - } -}); +// Actually apply the routes. +app.use(MOUNT_PATH, routes); module.exports = app; diff --git a/config.js b/config.js index 4472af26e..a2335822d 100644 --- a/config.js +++ b/config.js @@ -85,6 +85,10 @@ const CONFIG = { // The URL for this Talk Instance as viewable from the outside. ROOT_URL: process.env.TALK_ROOT_URL || null, + // ROOT_URL_MOUNT_PATH when TRUE will extract the pathname from the + // TALK_ROOT_URL and use it to mount the paths on. + ROOT_URL_MOUNT_PATH: process.env.TALK_ROOT_URL_MOUNT_PATH === 'TRUE', + // The keepalive timeout (in ms) that should be used to send keep alive // messages through the websocket to keep the socket alive. KEEP_ALIVE: process.env.TALK_KEEP_ALIVE || '30s', @@ -129,6 +133,10 @@ const CONFIG = { // CONFIG VALIDATION //============================================================================== +if (CONFIG.ROOT_URL_MOUNT_PATH && !CONFIG.ROOT_URL) { + throw new Error('TALK_ROOT_URL must be specified if TALK_ROOT_URL_MOUNT_PATH is set to TRUE'); +} + if (process.env.NODE_ENV === 'test' && !CONFIG.ROOT_URL) { CONFIG.ROOT_URL = 'http://localhost:3000'; } else if (!CONFIG.ROOT_URL) { @@ -169,14 +177,14 @@ if (CONFIG.JWT_DISABLE_ISSUER) { // External database url's //------------------------------------------------------------------------------ -// Reset the mongo url in the event it hasn't been overrided and we are in a +// Reset the mongo url in the event it hasn't been overridden and we are in a // testing environment. Every new mongo instance comes with a test database by // default, this is consistent with common testing and use case practices. if (process.env.NODE_ENV === 'test' && !CONFIG.MONGO_URL) { CONFIG.MONGO_URL = 'mongodb://localhost/test'; } -// Reset the redis url in the event it hasn't been overrided and we are in a +// Reset the redis url in the event it hasn't been overridden and we are in a // testing environment. if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) { CONFIG.REDIS_URL = 'redis://localhost/1'; diff --git a/docs/_docs/00-01-faq.md b/docs/_docs/00-01-faq.md index cdaf5dbf3..01172f84a 100644 --- a/docs/_docs/00-01-faq.md +++ b/docs/_docs/00-01-faq.md @@ -5,39 +5,63 @@ permalink: /docs/faq/ ### How are new stories/assets added to Talk? Is there an API? -There are three ways that new assets can make their way into Talk: _just in time_, _active_ and manual. +There are three ways that new assets can make their way into Talk: + +- Just in time +- Active +- Manual #### Just in Time asset creation -Talk ships with a _just in time_ mechanism that works out of the box without integration with any CMS or manual work needed. +Talk ships with a _just in time_ mechanism that works out of the box without +integration with any CMS or manual work needed. The _just in time_ flow looks like this: * Request comes in for a stream on an asset that doesn't yet exist. * Talk screens the domain against the domain whitelist, fails if doesn't pass. * Then, concurrently - * Talk creates a new asset record and returns the stream data (which will be empty) + * Talk creates a new asset record and returns the stream data (which will be + empty) * Schedules a job to scrape the new page and fill in asset information. -The scraping mechanism utilizes [metascraper](https://www.npmjs.com/package/metascraper) and is queued using the [Que](https://www.npmjs.com/package/kue). If your Talk deployments is configured to run separate job worker cluster, scraping will be performed by them. +The scraping mechanism utilizes +[metascraper](https://www.npmjs.com/package/metascraper) and is queued using the +[Que](https://www.npmjs.com/package/kue). If your Talk deployments is configured +to run separate job worker cluster, scraping will be performed by them. #### Active (or push based) asset creation -If tighter CMS integration is required to push custom data into assets and/or keep data in sync as changes are made in a CMS an _active_ push based workflow must be implemented. +If tighter CMS integration is required to push custom data into assets and/or +keep data in sync as changes are made in a CMS an _active_ push based workflow +must be implemented. -This is an ideal candidate for a plugin. If you are interested in working on it, please [contact us](https://coralproject.net/contact)! +This is an ideal candidate for a plugin. If you are interested in working on it, +please [contact us](https://coralproject.net/contact)! #### Manual asset creation -Sometimes you want to load a lot of assets into the database. The most common use case for this is populating the database during an initial installation. We recommend writing a script that transforms the data from it's source and inserts it into the _assets_ collection. +Sometimes you want to load a lot of assets into the database. The most common +use case for this is populating the database during an initial installation. We +recommend writing a script that transforms the data from it's source and inserts +it into the _assets_ collection. -For current schema information, please see the [asset model](https://github.com/coralproject/talk/blob/master/models/asset.js). +For current schema information, please see the +[asset model](https://github.com/coralproject/talk/blob/master/models/asset.js). ### Where are your http API docs? -Coral relies on GraphQL for the vast majority of it's client <-> server communication. All core queries, mutations and subscriptions are defined along with types and comments in our central [TypeDef](https://github.com/coralproject/talk/blob/master/graph/typeDefs.graphql). For plugin graph api typedefs, see each plugin's `/server/` directory. +Coral relies on GraphQL for the vast majority of it's client and server +communication. All core queries, mutations and subscriptions are defined along +with types and comments in our central +[TypeDef](https://github.com/coralproject/talk/blob/master/graph/typeDefs.graphql). +For plugin graph api typedefs, see each plugin's `/server/` directory. -In addition, Talk Server ships with [GraphiQL](https://github.com/graphql/graphiql). GraphiQL provides a full data layer IDE including interactive documentation. The autocompletes and documentation are populated from introspection meaning that Core _and plugin_ apis will be fully explorable. +In addition, Talk Server ships with +[GraphiQL](https://github.com/graphql/graphiql). GraphiQL provides a full data +layer IDE including interactive documentation. The autocomplete and +documentation are populated from introspection meaning that Core _and plugin_ +apis can be explored. To access GraphiQL: @@ -46,22 +70,33 @@ To access GraphiQL: ### Where is documentation for a specific component? -We strive for clear inline documentation across our codebase, but have gaps. Contributions to documentation would be greatly appreciated and is a great way to start contributing to the project! +We strive for clear inline documentation across our codebase, but have gaps. +Contributions to documentation would be greatly appreciated and is a great way +to start contributing to the project! -If you are considering changing a core component (aka, one that is not in a plugin), you are entering the realm of a core developer. We strongly ask that you reach out the coral team before forking and changing core code. We are glad to help talk through your product need and come up with a strategy for implementing as a plugin, or working with you to extend the plugin API for your use case. +If you are considering changing a core component (aka, one that is not in a +plugin), you are entering the realm of a core developer. We strongly ask that +you reach out the coral team before forking and changing core code. We are glad +to help talk through your product need and come up with a strategy for +implementing as a plugin, or working with you to extend the plugin API for your +use case. ### How do I contribute to these docs? -Contributions to the docs are much appreciated and a great way to get involved in the project. +Contributions to the docs are much appreciated and a great way to get involved +in the project. -Fork the Talk repo, clone it locally (no need to go through the install from source process), then: +Fork the Talk repo, clone it locally (no need to go through the install from +source process), then: ``` cd docs docker run --rm --volume=$PWD:/srv/jekyll -p 127.0.0.1:4000:4000 -it jekyll/jekyll:pages jekyll serve ``` -You can edit the files in docs with any editor and view the live updates in a browser by hitting From the docs directory. -Then visit: [http://127.0.0.1:4000/talk/](http://127.0.0.1:4000/talk/). +You can edit the files in docs with any editor and view the live updates in a +browser by hitting From the docs directory. Then visit: +[http://127.0.0.1:4000/talk/](http://127.0.0.1:4000/talk/). -Once you've made the changes, file a PR back to the `coralproject/talk` repo. +Once you've made the changes, file a PR back to the `coralproject/talk` +repository. diff --git a/docs/_docs/01-02-install-docker.md b/docs/_docs/01-02-install-docker.md index cf9c1fa0e..533f9ec16 100644 --- a/docs/_docs/01-02-install-docker.md +++ b/docs/_docs/01-02-install-docker.md @@ -11,8 +11,9 @@ Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Images are tagged using the following notation: -- `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting - new features occasionally and all the bug fixes, this is the tag for you. +- `x` (where `x` is the major version number): any minor or patch updates will + be included in this. If you're ok getting new features occasionally and all + the bug fixes, this is the tag for you. - `x.y` (where `y` is the minor version number): any patch updates will be included with this tag. If you like getting fixes and having features change only when you want, this is the tag for you. **(recommended)** diff --git a/docs/_docs/01-03-install-source.md b/docs/_docs/01-03-install-source.md index 3f05478ce..66dc94db6 100644 --- a/docs/_docs/01-03-install-source.md +++ b/docs/_docs/01-03-install-source.md @@ -36,7 +36,7 @@ git clone https://github.com/coralproject/talk.git We now have to install the dependencies and build the static assets. ```bash -# Install package dependancies +# Install package dependencies yarn # Build static files diff --git a/docs/_docs/01-05-install-microservices.md b/docs/_docs/01-05-install-microservices.md index 8e80a13ac..dc5ff112e 100644 --- a/docs/_docs/01-05-install-microservices.md +++ b/docs/_docs/01-05-install-microservices.md @@ -70,7 +70,7 @@ independently. Each variety of process can always have just enough resources. An install that heavily utilizes the jobs queue could see delays in http service because of heavy jobs processes and/or delays in the execution of jobs processes -due to increased server load as a result of Node's single thread infrustructure. +due to increased server load as a result of Node's single thread infrastructure. ## Deployment Methodologies diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index 74bd8972e..ac614fdb1 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -55,11 +55,18 @@ These are only used during the webpack build. ### Server - `TALK_ROOT_URL` (*required*) - root url of the installed application externally - available in the format: `://` without the path. + available in the format: `://:/`. - `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`) - `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - When `TRUE`, disables the dynamic setup endpoint. (Default `FALSE`) +#### Advanced + +- `TALK_ROOT_URL_MOUNT_PATH` (_optional_) - when set to `TRUE`, the routes will + be mounted onto the `` component of the `TALK_ROOT_URL`. You would + use this when your upstream proxy cannot strip the prefix from the url. + (Default `FALSE`) + ### Word Filter - `TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS` (_optional_) When `TRUE`, disables flagging of comments that match the suspect word filter. (Default `FALSE`) @@ -110,7 +117,7 @@ will be used: "iss": TALK_JWT_ISSUER, // *optional* if TALK_JWT_DISABLE_ISSUER === 'TRUE', *required* otherwise [TALK_JWT_USER_ID_CLAIM]: "", // *required* the id of the user - // Note, if TALK_JWT_USER_ID_CLAIM contains '.', it will be used to deliniate an object, for example + // Note, if TALK_JWT_USER_ID_CLAIM contains '.', it will be used to delineate an object, for example // `user.id` would store it like: `{user: {id}}` } ``` @@ -135,8 +142,8 @@ will be used: ### Trust -Trust can automoderate comments based on user history. By specifying this -option, the beheviour can be changed to offer different results. +Trust can auto-moderate comments based on user history. By specifying this +option, the behavior can be changed to offer different results. - `TRUST_THRESHOLDS` (_optional_) - configure the reliability thresholds for flagging and commenting. (Default `comment:-1,-1;flag:-1,-1`) @@ -150,10 +157,10 @@ The form of the environment variable: The default could be read as: - When a commenter has one comment rejected, their next comment must be - premoderated once in order to post freely again. If they instead get rejected + pre-moderated once in order to post freely again. If they instead get rejected again, then they must have two of their comments approved in order to get added back to the queue. -- At the moment of writing, beheviour is not attached to the flagging +- At the moment of writing, behavior is not attached to the flagging reliability, but it is recorded. ### Cache diff --git a/docs/_docs/02-02-secrets.md b/docs/_docs/02-02-secrets.md index c974c350f..42c1946b6 100644 --- a/docs/_docs/02-02-secrets.md +++ b/docs/_docs/02-02-secrets.md @@ -67,7 +67,7 @@ for more details. ## Authentication Types -Talk also supports two methods of providing authenticationd details. +Talk also supports two methods of providing authentication details. - Single key: this is used when your secrets do not need to be rotated. - Multiple keys: this is used when you expect to rotate your secrets. diff --git a/docs/_docs/03-05-client-architecture.md b/docs/_docs/03-05-client-architecture.md index 6bb44bfad..6392c20d0 100644 --- a/docs/_docs/03-05-client-architecture.md +++ b/docs/_docs/03-05-client-architecture.md @@ -32,7 +32,7 @@ It basically consist in having two types of components: ### Container Components * __How things work__ * They don’t have markup nor styles -* They provide data and behaviour to Presentational or Container Components +* They provide data and behavior to Presentational or Container Components * They connect via `react-redux`’s `connect()` to the state. * They `mapStateToProps` the state to the Presentational Container. * They `mapDispatchToProps` to send actions to the Presentational Container. diff --git a/docs/_docs/04-01-plugins.md b/docs/_docs/04-01-plugins.md index 8b9b1c99a..3182f6e3a 100644 --- a/docs/_docs/04-01-plugins.md +++ b/docs/_docs/04-01-plugins.md @@ -82,7 +82,7 @@ need to reconcile the plugins and build the static assets: # get plugin dependancies and remote plugins ./bin/cli plugins reconcile -# build staic assets (including enabled client side plugins) +# build static assets (including enabled client side plugins) yarn build ``` diff --git a/docs/_docs/04-02-plugins-quickstart.md b/docs/_docs/04-02-plugins-quickstart.md index cb2d2800b..ec5e23fef 100644 --- a/docs/_docs/04-02-plugins-quickstart.md +++ b/docs/_docs/04-02-plugins-quickstart.md @@ -236,6 +236,6 @@ Once you've taken this step, anyone can register your plugin into their Talk ser ### Publish to version control -This plugin is open source, so I'm also going to [publish it to github](https://github.com/jde/talk-plugin-asset-manager/commit/66b626caa85cb8030b3ddaa7c1a4821bf01e350a) and [cut a release](https://github.com/jde/talk-plugin-asset-manager/releases/tag/v0.1) that mirrors the npm relese. +This plugin is open source, so I'm also going to [publish it to github](https://github.com/jde/talk-plugin-asset-manager/commit/66b626caa85cb8030b3ddaa7c1a4821bf01e350a) and [cut a release](https://github.com/jde/talk-plugin-asset-manager/releases/tag/v0.1) that mirrors the npm release. ## Done! diff --git a/docs/_docs/04-03-plugins-client.md b/docs/_docs/04-03-plugins-client.md index 052b90873..511ccde6c 100644 --- a/docs/_docs/04-03-plugins-client.md +++ b/docs/_docs/04-03-plugins-client.md @@ -218,7 +218,7 @@ Basic settings can be added via json configuration in a plugin. * Default value * Variable name -#### Advanced Custom Configuration (low prioritiy) +#### Advanced Custom Configuration (low priority) Users can inject configuration interfaces that they create into the configuration allowing for more advanced configuration. diff --git a/docs/_docs/04-04-plugins-server.md b/docs/_docs/04-04-plugins-server.md index 6f204e6b8..13df5564f 100644 --- a/docs/_docs/04-04-plugins-server.md +++ b/docs/_docs/04-04-plugins-server.md @@ -220,7 +220,7 @@ when a valid token is provided but a user can't be found in the database that matches the provided id. The function is async, and should return the user object that was created in the -database, or null if the user wasn't found. The `jwt` paramenter of the object +database, or null if the user wasn't found. The `jwt` parameter of the object is the unpacked token, while `token` is the original jwt token string. ### Routes @@ -314,14 +314,14 @@ module.exports = { const {passport} = require('services/passport'); /** - * Facebook auth endpoint, this will redirect the user immediatly to facebook + * Facebook auth endpoint, this will redirect the user immediately to facebook * for authorization. */ router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']})); /** * Facebook callback endpoint, this will send the user a html page designed to - * send back the user credentials upon sucesfull login. + * send back the user credentials upon successful login. */ router.get('/facebook/callback', (req, res, next) => { diff --git a/middleware/pubsub.js b/middleware/pubsub.js new file mode 100644 index 000000000..15f7c51a2 --- /dev/null +++ b/middleware/pubsub.js @@ -0,0 +1,13 @@ +const pubsub = require('../services/pubsub'); +const pubsubClient = pubsub.createClientFactory(); + +// To handle dependancy injection safer, we inject the pubsub handle onto the +// request object. +module.exports = (req, res, next) => { + + // Attach the pubsub handle to the requests. + req.pubsub = pubsubClient(); + + // Forward on the request. + next(); +}; diff --git a/routes/index.js b/routes/index.js index f1992e83d..f83dd37cb 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,12 +2,45 @@ const express = require('express'); const path = require('path'); const plugins = require('../services/plugins'); const debug = require('debug')('talk:routes'); +const authentication = require('../middleware/authentication'); +const {passport} = require('../services/passport'); +const pubsub = require('../middleware/pubsub'); +const i18n = require('../services/i18n'); +const enabled = require('debug').enabled; +const errors = require('../errors'); +const {createGraphOptions} = require('../graph'); +const accepts = require('accepts'); +const apollo = require('graphql-server-express'); const router = express.Router(); -router.use('/api/v1', require('./api')); -router.use('/admin', require('./admin')); -router.use('/embed', require('./embed')); +//============================================================================== +// STATIC FILES +//============================================================================== + +// If the application is in production mode, then add gzip rewriting for the +// content. +if (process.env.NODE_ENV === 'production') { + router.get('*.js', (req, res, next) => { + const accept = accepts(req); + if (accept.encoding(['gzip']) === 'gzip') { + + // Adjsut the headers on the request by adding a content type header + // because express won't be able to detect the mime-type with the .gz + // extension and we need to decalre support for the gzip encoding. + res.set('Content-Type', 'application/javascript'); + res.set('Content-Encoding', 'gzip'); + + // Rewrite the url so that the gzip version will be served instead. + req.url = `${req.url}.gz`; + } + + next(); + }); +} + +router.use('/client', express.static(path.join(__dirname, 'dist'))); +router.use('/public', express.static(path.join(__dirname, 'public'))); /** * Serves a file based on a relative path. @@ -21,6 +54,60 @@ router.get('/embed.js', serveFile('../dist/embed.js')); router.get('/embed.js.gz', serveFile('../dist/embed.js.gz')); router.get('/embed.js.map', serveFile('../dist/embed.js.map')); +//============================================================================== +// PASSPORT MIDDLEWARE +//============================================================================== + +const passportDebug = require('debug')('talk:passport'); + +// Install the passport plugins. +plugins.get('server', 'passport').forEach((plugin) => { + passportDebug(`added plugin '${plugin.plugin.name}'`); + + // Pass the passport.js instance to the plugin to allow it to inject it's + // functionality. + plugin.passport(passport); +}); + +// Setup the PassportJS Middleware. +router.use(passport.initialize()); + +// Attach the authentication middleware, this will be responsible for decoding +// (if present) the JWT on the request. +router.use('/api', authentication, pubsub); + +//============================================================================== +// GraphQL Router +//============================================================================== + +// GraphQL endpoint. +router.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions)); + +// Only include the graphiql tool if we aren't in production mode. +if (process.env.NODE_ENV !== 'production') { + + // Interactive graphiql interface. + router.use('/api/v1/graph/iql', (req, res) => { + res.render('graphiql', { + endpointURL: `${req.locals.BASE_URL}api/v1/graph/ql` + }); + }); + + // GraphQL documention. + router.get('/admin/docs', (req, res) => { + res.render('admin/docs'); + }); + +} + +//============================================================================== +// ROUTES +//============================================================================== + +router.use('/api/v1', require('./api')); +router.use('/admin', require('./admin')); +router.use('/embed', require('./embed')); + if (process.env.NODE_ENV !== 'production') { router.use('/assets', require('./assets')); @@ -43,4 +130,53 @@ plugins.get('server', 'router').forEach((plugin) => { plugin.router(router); }); +//============================================================================== +// ERROR HANDLING +//============================================================================== + +// Catch 404 and forward to error handler. +router.use((req, res, next) => { + next(errors.ErrNotFound); +}); + +// General api error handler. Respond with the message and error if we have it +// while returning a status code that makes sense. +router.use('/api', (err, req, res, next) => { + if (err !== errors.ErrNotFound) { + if (process.env.NODE_ENV !== 'test' || enabled('talk:errors')) { + console.error(err); + } + } + + if (err instanceof errors.APIError) { + res.status(err.status).json({ + message: err.message, + error: err + }); + } else { + res.status(500).json({}); + } +}); + +router.use('/', (err, req, res, next) => { + if (err !== errors.ErrNotFound) { + console.error(err); + } + + i18n.init(req); + + if (err instanceof errors.APIError) { + res.status(err.status); + res.render('error', { + message: err.message, + error: process.env.NODE_ENV === 'development' ? err : {} + }); + } else { + res.render('error', { + message: err.message, + error: process.env.NODE_ENV === 'development' ? err : {} + }); + } +}); + module.exports = router; From 8afbe9d30d6527e58256ceb00279f74a7e8c068c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 15 Aug 2017 14:40:12 -0600 Subject: [PATCH 043/109] applied path fixes to template --- views/admin.ejs | 2 +- views/admin/confirm-email.ejs | 2 +- views/admin/docs.ejs | 2 +- views/admin/password-reset.ejs | 2 +- views/article.ejs | 3 +-- views/articles.ejs | 2 +- views/embed/stream.ejs | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/views/admin.ejs b/views/admin.ejs index 0a7c1c218..2aa23aa58 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -42,6 +42,6 @@
- + diff --git a/views/admin/confirm-email.ejs b/views/admin/confirm-email.ejs index 20d213fa2..ed23d3dc8 100644 --- a/views/admin/confirm-email.ejs +++ b/views/admin/confirm-email.ejs @@ -71,7 +71,7 @@ $('.error-console').removeClass('active'); $.ajax({ - url: '/api/v1/account/email/verify', + url: '<%= BASE_PATH %>api/v1/account/email/verify', contentType: 'application/json', method: 'POST', data: JSON.stringify({token: location.hash.replace('#', '')}) diff --git a/views/admin/docs.ejs b/views/admin/docs.ejs index de288e055..d13dff064 100644 --- a/views/admin/docs.ejs +++ b/views/admin/docs.ejs @@ -28,6 +28,6 @@
- + diff --git a/views/admin/password-reset.ejs b/views/admin/password-reset.ejs index 704b5213d..f02e5b6f8 100644 --- a/views/admin/password-reset.ejs +++ b/views/admin/password-reset.ejs @@ -117,7 +117,7 @@ } $.ajax({ - url: '/api/v1/account/password/reset', + url: '<%= BASE_PATH %>api/v1/account/password/reset', contentType: 'application/json', method: 'PUT', data: JSON.stringify({password: password, token: location.hash.replace('#', '')}) diff --git a/views/article.ejs b/views/article.ejs index 098a95524..f0a3dad98 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -17,7 +17,6 @@ } <%= title %> -
@@ -25,7 +24,7 @@

<%= body %>

Admin - All Assets

- + From 76e1d55601befb6e98105e77470b51fbc6312a60 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 16 Aug 2017 07:53:58 -0600 Subject: [PATCH 044/109] fixed broken route --- routes/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/index.js b/routes/index.js index f83dd37cb..dafaf09b9 100644 --- a/routes/index.js +++ b/routes/index.js @@ -39,8 +39,8 @@ if (process.env.NODE_ENV === 'production') { }); } -router.use('/client', express.static(path.join(__dirname, 'dist'))); -router.use('/public', express.static(path.join(__dirname, 'public'))); +router.use('/client', express.static(path.join(__dirname, '../dist'))); +router.use('/public', express.static(path.join(__dirname, '../public'))); /** * Serves a file based on a relative path. From 9ce6ab108a3b7c2ffe65fbbbdeaa73108ea8eafd Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 16 Aug 2017 22:07:25 +0700 Subject: [PATCH 045/109] Refactor StreamTabPanel and use hoistStatics for all hocs --- .../src/components/Comment.js | 2 +- .../src/components/Stream.js | 131 ++++++------------ .../src/components/StreamTabPanel.js | 23 +++ .../src/containers/Stream.js | 8 +- .../src/containers/StreamTabPanel.js | 84 +++++++++++ .../hocs/withCopyToClipboard.js | 5 +- client/coral-framework/hocs/withEmit.js | 7 +- client/coral-framework/hocs/withFragments.js | 24 +++- client/coral-framework/hocs/withMutation.js | 5 +- client/coral-framework/hocs/withQuery.js | 5 +- plugin-api/beta/client/hocs/withTags.js | 5 +- 11 files changed, 195 insertions(+), 104 deletions(-) create mode 100644 client/coral-embed-stream/src/components/StreamTabPanel.js create mode 100644 client/coral-embed-stream/src/containers/StreamTabPanel.js diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6c231f4ae..b8063858d 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -72,7 +72,7 @@ const ActionButton = ({children}) => { ); }; -export default class Comment extends React.Component { +export default class Comment extends React.PureComponent { constructor(props) { super(props); diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 7b9e5f566..a9ea98a68 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -10,16 +10,16 @@ import {ModerationLink} from 'talk-plugin-moderation'; import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox'; import t, {timeago} from 'coral-framework/services/i18n'; -import {getSlotComponents} from 'coral-framework/helpers/plugins'; import CommentBox from 'talk-plugin-commentbox/CommentBox'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; import {isCommentActive} from 'coral-framework/utils'; -import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui'; +import {Button, Tab, TabCount, TabPane} from 'coral-ui'; import cn from 'classnames'; import {getTopLevelParent, attachCommentToParent} from '../graphql/utils'; import AllCommentsPane from './AllCommentsPane'; import AutomaticAssetClosure from '../containers/AutomaticAssetClosure'; +import StreamTabPanel from '../containers/StreamTabPanel'; import styles from './Stream.css'; @@ -35,44 +35,9 @@ class Stream extends React.Component { componentWillReceiveProps(next) { // Keep comment box when user was live suspended, banned, ... - if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) { + if (!this.props.userIsDegraged && next.userIsDegraged) { this.setState({keepCommentBox: true}); } - - this.fallbackAllTab(next); - } - - componentDidMount() { - this.fallbackAllTab(); - } - - fallbackAllTab(props = this.props) { - if (props.activeStreamTab !== 'all') { - const slotPlugins = this.getSlotComponents('streamTabs', props).map((c) => c.talkPluginName); - if (slotPlugins.indexOf(props.activeStreamTab) === -1) { - props.setActiveStreamTab('all'); - } - } - } - - getSlotProps({data, root, root: {asset}} = this.props) { - return {data, root, asset}; - } - - getSlotComponents(slot, props = this.props) { - return getSlotComponents(slot, props.reduxState, this.getSlotProps(props)); - } - - setActiveReplyBox = (id) => { - if (!this.props.auth.user) { - this.props.showSignInDialog(); - } else { - this.props.setActiveReplyBox(id); - } - }; - - userIsDegraged({auth: {user}} = this.props) { - return !can(user, 'INTERACT_WITH_COMMUNITY'); } render() { @@ -99,7 +64,7 @@ class Stream extends React.Component { loadMoreComments, viewAllComments, auth: {loggedIn, user}, - editName + editName, } = this.props; const {keepCommentBox} = this.state; const open = !asset.isClosed; @@ -133,7 +98,7 @@ class Stream extends React.Component { }; const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox); - const slotProps = this.getSlotProps(); + const slotProps = {data, root, asset}; if (!comment && !comments) { console.error('Talk: No comments came back from the graph given that query. Please, check the query params.'); @@ -247,55 +212,49 @@ class Stream extends React.Component { {...slotProps} />
- - {this.getSlotComponents('streamTabs').map((PluginComponent) => ( - - + + All Comments {totalCommentCount} - ))} - - All Comments {totalCommentCount} - - - - {this.getSlotComponents('streamTabPanes').map((PluginComponent) => ( - - + - ))} - - - - + } + sub + />
}
diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.js b/client/coral-embed-stream/src/components/StreamTabPanel.js new file mode 100644 index 000000000..c5a292179 --- /dev/null +++ b/client/coral-embed-stream/src/components/StreamTabPanel.js @@ -0,0 +1,23 @@ +import React from 'react'; +import {TabBar, TabContent} from 'coral-ui'; + +class StreamTabPanel extends React.Component { + + render() { + const {activeTab, setActiveTab, appendTabs, appendTabPanes, pluginTabElements, pluginTabPaneElements, sub} = this.props; + return ( +
+ + {pluginTabElements} + {appendTabs} + + + {pluginTabPaneElements} + {appendTabPanes} + +
+ ); + } +} + +export default StreamTabPanel; diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 451b115a9..acc178261 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -16,6 +16,7 @@ import Comment from './Comment'; import {withFragments, withEmit} from 'coral-framework/hocs'; import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {Spinner} from 'coral-ui'; +import {can} from 'coral-framework/services/perms'; import { findCommentInEmbedQuery, insertCommentIntoEmbedQuery, @@ -23,7 +24,6 @@ import { insertFetchedCommentsIntoEmbedQuery, nest, } from '../graphql/utils'; -import omit from 'lodash/omit'; const {showSignInDialog, editName} = authActions; const {addNotification} = notificationActions; @@ -140,6 +140,10 @@ class StreamContainer extends React.Component { clearInterval(this.countPoll); } + userIsDegraged({auth: {user}} = this.props) { + return !can(user, 'INTERACT_WITH_COMMUNITY'); + } + render() { if (this.props.refetching) { return ; @@ -149,6 +153,7 @@ class StreamContainer extends React.Component { loadMore={this.loadMore} loadMoreComments={this.loadMoreComments} loadNewReplies={this.loadNewReplies} + userIsDegraged={this.userIsDegraged()} />; } } @@ -310,7 +315,6 @@ const mapStateToProps = (state) => ({ previousStreamTab: state.stream.previousTab, commentClassNames: state.stream.commentClassNames, pluginConfig: state.config.plugin_config, - reduxState: omit(state, 'apollo'), }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js new file mode 100644 index 000000000..574c4c881 --- /dev/null +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -0,0 +1,84 @@ +import React from 'react'; +import StreamTabPanel from '../components/StreamTabPanel'; +import {connect} from 'react-redux'; +import omit from 'lodash/omit'; +import {getSlotComponents} from 'coral-framework/helpers/plugins'; +import {Tab, TabPane} from 'coral-ui'; +import {getShallowChanges} from 'coral-framework/utils'; +import isEqual from 'lodash/isEqual'; + +class StreamTabPanelContainer extends React.Component { + + componentDidMount() { + this.fallbackAllTab(); + } + + componentWillReceiveProps(next) { + this.fallbackAllTab(next); + } + + shouldComponentUpdate(next) { + + // Prevent Slot from rerendering when only reduxState has changed and + // it does not result in a change of slot children. + const changes = getShallowChanges(this.props, next); + if (changes.length === 1 && changes[0] === 'reduxState') { + const prevUuid = this.getSlotComponents(this.props.tabSlot, this.props).map((cmp) => cmp.talkUuid); + const nextUuid = this.getSlotComponents(next.tabSlot, next).map((cmp) => cmp.talkUuid); + return !isEqual(prevUuid, nextUuid); + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + fallbackAllTab(props = this.props) { + if (props.activeTab !== props.fallbackTab) { + const slotPlugins = this.getSlotComponents(props.tabSlot, props).map((c) => c.talkPluginName); + if (slotPlugins.indexOf(props.activeTab) === -1) { + props.setActiveTab(props.fallbackTab); + } + } + } + + getSlotComponents(slot, props = this.props) { + return getSlotComponents(slot, props.reduxState, props.slotProps); + } + + getPluginTabElements(props = this.props) { + return this.getSlotComponents(props.tabSlot).map((PluginComponent) => ( + + + + )); + } + + getPluginTabPaneElements(props = this.props) { + return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => ( + + + + )); + } + + render() { + return ( + + ); + } +} + +const mapStateToProps = (state) => ({ + reduxState: omit(state, 'apollo'), +}); + +export default connect(mapStateToProps, null)(StreamTabPanelContainer); diff --git a/client/coral-framework/hocs/withCopyToClipboard.js b/client/coral-framework/hocs/withCopyToClipboard.js index 9e8046802..4de606c40 100644 --- a/client/coral-framework/hocs/withCopyToClipboard.js +++ b/client/coral-framework/hocs/withCopyToClipboard.js @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Clipboard from 'clipboard'; +import hoistStatics from 'recompose/hoistStatics'; -export default (WrappedComponent) => { +export default hoistStatics((WrappedComponent) => { class WithCopyToClipboard extends React.Component { componentDidMount() { const clipboard = new Clipboard(ReactDOM.findDOMNode(this)); @@ -26,4 +27,4 @@ export default (WrappedComponent) => { } return WithCopyToClipboard; -}; +}); diff --git a/client/coral-framework/hocs/withEmit.js b/client/coral-framework/hocs/withEmit.js index 3a3216c8a..22d98bb27 100644 --- a/client/coral-framework/hocs/withEmit.js +++ b/client/coral-framework/hocs/withEmit.js @@ -1,11 +1,12 @@ import React from 'react'; -const PropTypes = require('prop-types'); +import hoistStatics from 'recompose/hoistStatics'; +import PropTypes from 'prop-types'; /** * WithEmit provides a property `emit: (eventName, value)` * to the wrapped component. */ -export default (WrappedComponent) => { +export default hoistStatics((WrappedComponent) => { class WithEmit extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, @@ -24,4 +25,4 @@ export default (WrappedComponent) => { } return WithEmit; -}; +}); diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index 62e96444c..c422f8705 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,5 +1,21 @@ // TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38. -export default (fragments) => (BaseComponent) => { - BaseComponent.fragments = fragments; - return BaseComponent; -}; + +import React from 'react'; +import {resolveFragments} from 'coral-framework/services/graphqlRegistry'; +import mapValues from 'lodash/mapValues'; +import hoistStatics from 'recompose/hoistStatics'; + +export default (fragments) => hoistStatics((BaseComponent) => { + class WithFragments extends React.Component { + fragments = mapValues(fragments, (val) => resolveFragments(val)); + + render() { + return ; + } + } + + WithFragments.fragments = fragments; + return WithFragments; +}); diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 7e2c3d09d..bb2321d11 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -8,6 +8,7 @@ import {getMutationOptions, resolveFragments} from 'coral-framework/services/gra import {getDefinitionName, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; +import hoistStatics from 'recompose/hoistStatics'; class ResponseErrors extends Error { constructor(errors) { @@ -30,7 +31,7 @@ class ResponseError { * Exports a HOC with the same signature as `graphql`, that will * apply mutation options registered in the graphRegistry. */ -export default (document, config = {}) => (WrappedComponent) => { +export default (document, config = {}) => hoistStatics((WrappedComponent) => { config = { ...config, options: config.options || {}, @@ -147,4 +148,4 @@ export default (document, config = {}) => (WrappedComponent) => { return ; } }; -}; +}); diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index d98400419..f19c3209e 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -3,6 +3,7 @@ import {graphql} from 'react-apollo'; import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry'; import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; +import hoistStatics from 'recompose/hoistStatics'; const withSkipOnErrors = (reducer) => (prev, action, ...rest) => { if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) { @@ -35,7 +36,7 @@ function networkStatusToString(networkStatus) { * Exports a HOC with the same signature as `graphql`, that will * apply query options registered in the graphRegistry. */ -export default (document, config = {}) => (WrappedComponent) => { +export default (document, config = {}) => hoistStatics((WrappedComponent) => { const name = getDefinitionName(document); return class WithQuery extends React.Component { @@ -190,4 +191,4 @@ export default (document, config = {}) => (WrappedComponent) => { return ; } }; -}; +}); diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index b0a74f4d7..428c3f43f 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -8,8 +8,9 @@ import {withAddTag, withRemoveTag} from 'coral-framework/graphql/mutations'; import withFragments from 'coral-framework/hocs/withFragments'; import {addNotification} from 'coral-framework/actions/notification'; import {forEachError, isTagged} from 'coral-framework/utils'; +import hoistStatics from 'recompose/hoistStatics'; -export default (tag) => (WrappedComponent) => { +export default (tag) => hoistStatics((WrappedComponent) => { if (typeof tag !== 'string') { console.error('Tag must be a valid string'); return null; @@ -109,4 +110,4 @@ export default (tag) => (WrappedComponent) => { WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`; return enhance(WithTags); -}; +}); From 4aa6b530228352dbcd3f6387fcd88582d7866675 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 16 Aug 2017 22:32:44 +0700 Subject: [PATCH 046/109] PropTypes for StreamTabPanel --- .../src/components/Stream.js | 4 +-- .../src/components/StreamTabPanel.js | 24 ++++++++++++---- .../src/containers/StreamTabPanel.js | 28 +++++++++++++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index a9ea98a68..26b054e0d 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -220,12 +220,12 @@ class Stream extends React.Component { tabPaneSlot={'streamTabPanes'} slotProps={slotProps} appendTabs={ - + All Comments {totalCommentCount} } appendTabPanes={ - + - {pluginTabElements} - {appendTabs} + {tabs} - {pluginTabPaneElements} - {appendTabPanes} + {tabPanes}
); } } +StreamTabPanel.propTypes = { + activeTab: PropTypes.string.isRequired, + setActiveTab: PropTypes.func.isRequired, + tabs: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + tabPanes: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + className: PropTypes.string, + sub: PropTypes.bool, +}; + export default StreamTabPanel; diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js index 574c4c881..4a313d2b4 100644 --- a/client/coral-embed-stream/src/containers/StreamTabPanel.js +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -6,6 +6,7 @@ import {getSlotComponents} from 'coral-framework/helpers/plugins'; import {Tab, TabPane} from 'coral-ui'; import {getShallowChanges} from 'coral-framework/utils'; import isEqual from 'lodash/isEqual'; +import PropTypes from 'prop-types'; class StreamTabPanelContainer extends React.Component { @@ -69,14 +70,35 @@ class StreamTabPanelContainer extends React.Component { render() { return ( ); } } +StreamTabPanelContainer.propTypes = { + activeTab: PropTypes.string.isRequired, + setActiveTab: PropTypes.func.isRequired, + appendTabs: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + appendTabPanes: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + fallbackTab: PropTypes.string.isRequired, + tabSlot: PropTypes.string.isRequired, + tabPaneSlot: PropTypes.string.isRequired, + className: PropTypes.string, + sub: PropTypes.bool, +}; + const mapStateToProps = (state) => ({ reduxState: omit(state, 'apollo'), }); From ae81d5cb3c703d13832c508d79b42e2c7fac9d08 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 16 Aug 2017 22:43:48 +0700 Subject: [PATCH 047/109] Optimize IfSlotIsX rendering --- .../components/IfSlotIsEmpty.js | 35 +++++++++++++++---- .../components/IfSlotIsNotEmpty.js | 35 +++++++++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index 424a59bc7..e9211fc57 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -3,13 +3,36 @@ import {connect} from 'react-redux'; import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; +import {getShallowChanges} from 'coral-framework/utils'; -function IfSlotIsEmpty({slot, className, reduxState, component: Component = 'div', children, ...rest}) { - return ( - - {isSlotEmpty(slot, reduxState, rest) ? children : null} - - ); +class IfSlotIsEmpty extends React.Component { + + shouldComponentUpdate(next) { + + // Prevent Slot from rerendering when only reduxState has changed and + // it does not result in a change. + const changes = getShallowChanges(this.props, next); + if (changes.length === 1 && changes[0] === 'reduxState') { + return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next); + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + isSlotEmpty(props = this.props) { + const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; + return isSlotEmpty(slot, reduxState, rest); + } + + render() { + const {className, component: Component = 'div', children} = this.props; + return ( + + {this.isSlotEmpty() ? children : null} + + ); + } } IfSlotIsEmpty.propTypes = { diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 3a41fffbd..e7a0e83ce 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -3,13 +3,36 @@ import {connect} from 'react-redux'; import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; +import {getShallowChanges} from 'coral-framework/utils'; -function IfSlotIsNotEmpty({slot, className, reduxState, component: Component = 'div', children, ...rest}) { - return ( - - {!isSlotEmpty(slot, reduxState, rest) ? children : null} - - ); +class IfSlotIsNotEmpty extends React.Component { + + shouldComponentUpdate(next) { + + // Prevent Slot from rerendering when only reduxState has changed and + // it does not result in a change. + const changes = getShallowChanges(this.props, next); + if (changes.length === 1 && changes[0] === 'reduxState') { + return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next); + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + isSlotEmpty(props = this.props) { + const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; + return isSlotEmpty(slot, reduxState, rest); + } + + render() { + const {className, component: Component = 'div', children} = this.props; + return ( + + {this.isSlotEmpty() ? null : children} + + ); + } } IfSlotIsNotEmpty.propTypes = { From f8031be86589e3cbdebfadb862eae77a8a1748b3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 16 Aug 2017 23:41:14 +0700 Subject: [PATCH 048/109] callback props from withMutation now keep their identity --- .../src/components/Comment.js | 2 +- client/coral-framework/hocs/withMutation.js | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index b8063858d..6c231f4ae 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -72,7 +72,7 @@ const ActionButton = ({children}) => { ); }; -export default class Comment extends React.PureComponent { +export default class Comment extends React.Component { constructor(props) { super(props); diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index bb2321d11..7c3dcfa1a 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -9,6 +9,7 @@ import {getDefinitionName, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; import hoistStatics from 'recompose/hoistStatics'; +import union from 'lodash/union'; class ResponseErrors extends Error { constructor(errors) { @@ -47,7 +48,13 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; - wrappedProps = (data) => { + // Props as we would pass to the BaseComponent without optimizations. + dynamicProps = {}; + + // Props that are optimized by keeping the identity of function callbacks. + staticProps = {}; + + propsWrapper = (data) => { const name = getDefinitionName(document); const callbacks = getMutationOptions(name); const mutate = (base) => { @@ -133,12 +140,34 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { throw error; }); }; - return config.props({...data, mutate}); + + // Save current props to `dynamicProps` + this.dynamicProps = config.props({...data, mutate}); + + // Sync props to `staticProps`. + // `staticProps` ultimately contains the same props as `dynamicProps` but all callbacks + // keep their identity. + union(Object.keys(this.dynamicProps), Object.keys(this.staticProps)).forEach((key) => { + if (!(key in this.dynamicProps)) { + delete this.staticProps[key]; + return; + } + if (typeof this.dynamicProps[key] !== 'function') { + this.staticProps[key] = this.dynamicProps[key]; + return; + } + + if (!(key in this.staticProps)) { + this.staticProps[key] = (...args) => this.dynamicProps[key](...args); + return; + } + }); + return this.staticProps; }; getWrapped = () => { if (!this.memoized) { - this.memoized = graphql(resolveFragments(document), {...config, props: this.wrappedProps})(WrappedComponent); + this.memoized = graphql(resolveFragments(document), {...config, props: this.propsWrapper})(WrappedComponent); } return this.memoized; }; From 98e2cb33879403a2b043d724d70244369a0b8926 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 17 Aug 2017 10:28:12 +0100 Subject: [PATCH 049/109] Version 3.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6b8a782d..71477361e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "3.1.0", + "version": "3.2.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { From df141925fb755544f19b4999c0be3c73eac0eca2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 17 Aug 2017 17:15:06 +0700 Subject: [PATCH 050/109] add hoistStatics to withReaction --- plugin-api/beta/client/hocs/withReaction.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 22c30d639..9ac49cf43 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -11,9 +11,10 @@ import {showSignInDialog} from 'coral-framework/actions/auth'; import {addNotification} from 'coral-framework/actions/notification'; import {capitalize} from 'coral-framework/helpers/strings'; import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; +import hoistStatics from 'recompose/hoistStatics'; import * as PropTypes from 'prop-types'; -export default (reaction) => (WrappedComponent) => { +export default (reaction) => hoistStatics((WrappedComponent) => { if (typeof reaction !== 'string') { console.error('Reaction must be a valid string'); return null; @@ -391,4 +392,4 @@ export default (reaction) => (WrappedComponent) => { WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`; return enhance(WithReactions); -}; +}); From 1ff6c5bdcc7aa95a637b31189f5a56bbb039cc66 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 17 Aug 2017 17:16:15 +0700 Subject: [PATCH 051/109] Use comment container --- .../src/components/AllCommentsPane.js | 2 +- .../src/components/Comment.js | 23 ++------- .../src/components/Stream.js | 2 +- .../src/containers/Comment.js | 47 +++++++++++++++++-- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index 6c5d6bb23..c141f0618 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -5,7 +5,7 @@ import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import NewCount from './NewCount'; import {TransitionGroup} from 'react-transition-group'; import {forEachError} from 'coral-framework/utils'; -import Comment from '../components/Comment'; +import Comment from '../containers/Comment'; const hasComment = (nodes, id) => nodes.some((node) => node.id === id); diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6c231f4ae..39c3b5edf 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -24,6 +24,7 @@ import InactiveCommentLabel from './InactiveCommentLabel'; import {EditableCommentContent} from './EditableCommentContent'; import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; +import CommentContainer from '../containers/Comment'; const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF'); const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length; @@ -86,7 +87,6 @@ export default 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, loadingState: '', ...resetCursors({}, props), }; @@ -112,22 +112,6 @@ export default class Comment extends React.Component { } } - 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 = { // id of currently opened ReplyBox. tracked in Stream.js @@ -315,6 +299,7 @@ export default class Comment extends React.Component { showSignInDialog, liveUpdates, commentIsIgnored, + animateEnter, commentClassNames = [] } = this.props; @@ -367,7 +352,7 @@ export default class Comment extends React.Component { styles[`rootLevel${depth}`], { ...conditionalClassNames, - [styles.enter]: this.state.animateEnter, + [styles.enter]: animateEnter, }, ); @@ -542,7 +527,7 @@ export default class Comment extends React.Component { {view.map((reply) => { return commentIsIgnored(reply) ? - : { + class WithAnimateEnter extends React.Component { + state = { + animateEnter: false, + }; + + 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}); + } + + render() { + return ; + } + } + return WithAnimateEnter; +}); + +const withCommentFragments = withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { __typename @@ -57,4 +91,11 @@ export default withFragments({ ${getSlotFragmentSpreads(slots, 'comment')} } ` -})(Comment); +}); + +const enhance = compose( + withAnimateEnter, + withCommentFragments, +); + +export default enhance(Comment); From 2022be41349e99a36d7bf52e120e1aacb7fb35c6 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 17 Aug 2017 12:04:09 +0100 Subject: [PATCH 052/109] Use latest version of Talk in examples --- docs/_docs/01-02-install-docker.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_docs/01-02-install-docker.md b/docs/_docs/01-02-install-docker.md index 533f9ec16..9d177049e 100644 --- a/docs/_docs/01-02-install-docker.md +++ b/docs/_docs/01-02-install-docker.md @@ -43,7 +43,7 @@ An example docker-compose.yml: version: '2' services: talk: - image: coralproject/talk:1.5 + image: coralproject/talk:latest restart: always ports: - "5000:5000" @@ -86,7 +86,7 @@ on different machines. You can achieve this easily with docker compose: version: '2' services: talk-api: - image: coralproject/talk:1.5 + image: coralproject/talk:latest command: cli serve restart: always ports: @@ -98,7 +98,7 @@ services: - TALK_MONGO_URL=mongodb://mongo/talk - TALK_REDIS_URL=redis://redis talk-jobs: - image: coralproject/talk:1.5 + image: coralproject/talk:latest command: cli jobs process restart: always ports: From 7ea6d133c2e7a7693a2201a8d7b257ecb39eefad Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 17 Aug 2017 21:31:09 +0700 Subject: [PATCH 053/109] Filter queryData in withFragments and optimize rendering --- .../src/components/Comment.js | 18 +++- .../src/components/Stream.js | 29 ++++--- .../src/containers/Comment.js | 68 +++++++++------ .../src/containers/Stream.js | 33 ++----- .../src/containers/StreamTabPanel.js | 10 ++- client/coral-framework/components/Slot.js | 17 ++-- client/coral-framework/helpers/plugins.js | 39 +++++++-- client/coral-framework/hocs/withFragments.js | 85 ++++++++++++++++++- plugin-api/beta/client/hocs/withReaction.js | 11 ++- plugin-api/beta/client/hocs/withTags.js | 6 ++ .../client/containers/TabPane.js | 4 +- 11 files changed, 227 insertions(+), 93 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 39c3b5edf..cc1ee7bed 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -224,6 +224,10 @@ export default class Comment extends React.Component { return; } + commentPostedHandler = () => { + this.props.setActiveReplyBox(''); + } + // getVisibileReplies returns a list containing comments // which were authored by current user or comes before the `idCursor`. getVisibileReplies() { @@ -372,10 +376,13 @@ export default class Comment extends React.Component { // props that are passed down the slots. const slotProps = { data, + depth, + }; + + const queryData = { root, asset, comment, - depth, }; return ( @@ -389,6 +396,7 @@ export default class Comment extends React.Component { className={`${styles.commentAvatar} talk-stream-comment-avatar`} fill="commentAvatar" {...slotProps} + queryData={queryData} inline /> @@ -411,6 +419,7 @@ export default class Comment extends React.Component { className={styles.commentInfoBar} fill="commentInfoBar" {...slotProps} + queryData={queryData} /> { isActive && (currentUser && (comment.user.id === currentUser.id)) && @@ -457,6 +466,7 @@ export default class Comment extends React.Component { fill="commentContent" defaultComponent={CommentContent} {...slotProps} + queryData={queryData} />
} @@ -468,6 +478,7 @@ export default class Comment extends React.Component { {!disableReply && @@ -484,6 +495,7 @@ export default class Comment extends React.Component { fill="commentActions" wrapperComponent={ActionButton} {...slotProps} + queryData={queryData} inline /> @@ -509,9 +521,7 @@ export default class Comment extends React.Component { {activeReplyBox === comment.id ? { - setActiveReplyBox(''); - }} + commentPostedHandler={this.commentPostedHandler} charCountEnable={charCountEnable} maxCharCount={maxCharCount} setActiveReplyBox={setActiveReplyBox} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index aa1221943..cf74813e9 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -40,6 +40,15 @@ class Stream extends React.Component { } } + commentIsIgnored = (comment) => { + const me = this.props.root.me; + return ( + me && + me.ignoredUsers && + me.ignoredUsers.find((u) => u.id === comment.user.id) + ); + }; + render() { const { data, @@ -48,7 +57,7 @@ class Stream extends React.Component { setActiveReplyBox, appendItemArray, commentClassNames, - root: {asset, asset: {comment, comments, totalCommentCount}, me}, + root: {asset, asset: {comment, comments, totalCommentCount}}, postComment, addNotification, editComment, @@ -89,16 +98,9 @@ class Stream extends React.Component { user.suspension.until && new Date(user.suspension.until) > new Date(); - const commentIsIgnored = (comment) => { - return ( - me && - me.ignoredUsers && - me.ignoredUsers.find((u) => u.id === comment.user.id) - ); - }; - const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox); - const slotProps = {data, root, asset}; + const slotProps = {data}; + const slotQueryData = {root, asset}; if (!comment && !comments) { console.error('Talk: No comments came back from the graph given that query. Please, check the query params.'); @@ -160,6 +162,7 @@ class Stream extends React.Component { @@ -194,7 +197,7 @@ class Stream extends React.Component { deleteAction={deleteAction} showSignInDialog={showSignInDialog} key={highlightedComment.id} - commentIsIgnored={commentIsIgnored} + commentIsIgnored={this.commentIsIgnored} comment={highlightedComment} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} @@ -209,6 +212,7 @@ class Stream extends React.Component { >
@@ -219,6 +223,7 @@ class Stream extends React.Component { tabSlot={'streamTabs'} tabPaneSlot={'streamTabPanes'} slotProps={slotProps} + queryData={slotQueryData} appendTabs={ All Comments {totalCommentCount} @@ -245,7 +250,7 @@ class Stream extends React.Component { loadNewReplies={loadNewReplies} deleteAction={deleteAction} showSignInDialog={showSignInDialog} - commentIsIgnored={commentIsIgnored} + commentIsIgnored={this.commentIsIgnored} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 40c2fa000..5f3ee0c9d 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -3,7 +3,9 @@ import React from 'react'; import Comment from '../components/Comment'; import {withFragments} from 'coral-framework/hocs'; import {getSlotFragmentSpreads} from 'coral-framework/utils'; +import {THREADING_LEVEL} from '../constants/stream'; import hoistStatics from 'recompose/hoistStatics'; +import {nest} from '../graphql/utils'; const slots = [ 'streamQuestionArea', @@ -48,6 +50,36 @@ const withAnimateEnter = hoistStatics((BaseComponent) => { return WithAnimateEnter; }); +const singleCommentFragment = gql` + fragment CoralEmbedStream_Comment_SingleComment on Comment { + id + body + created_at + status + replyCount + tags { + tag { + name + } + } + user { + id + username + } + action_summaries { + __typename + count + current_user { + id + } + } + editing { + edited + editableUntil + } + } +`; + const withCommentFragments = withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { @@ -63,33 +95,21 @@ const withCommentFragments = withFragments({ `, comment: gql` fragment CoralEmbedStream_Comment_comment on Comment { - id - body - created_at - status - replyCount - tags { - tag { - name + ...CoralEmbedStream_Comment_SingleComment + ${nest(` + replies(limit: 3, excludeIgnored: $excludeIgnored) { + nodes { + ...CoralEmbedStream_Comment_SingleComment + ...nest + } + hasNextPage + startCursor + endCursor } - } - user { - id - username - } - action_summaries { - __typename - count - current_user { - id - } - } - editing { - edited - editableUntil - } + `, THREADING_LEVEL)} ${getSlotFragmentSpreads(slots, 'comment')} } + ${singleCommentFragment} ` }); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index acc178261..f433bfb77 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -161,19 +161,11 @@ class StreamContainer extends React.Component { const commentFragment = gql` fragment CoralEmbedStream_Stream_comment on Comment { id + status + user { + id + } ...${getDefinitionName(Comment.fragments.comment)} - ${nest(` - replies(excludeIgnored: $excludeIgnored) { - nodes { - id - ...${getDefinitionName(Comment.fragments.comment)} - ...nest - } - hasNextPage - startCursor - endCursor - } - `, THREADING_LEVEL)} } ${Comment.fragments.comment} `; @@ -210,27 +202,14 @@ const LOAD_MORE_QUERY = gql` query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { nodes { - id - ...${getDefinitionName(Comment.fragments.comment)} - ${nest(` - replies(limit: 3, excludeIgnored: $excludeIgnored) { - nodes { - id - ...${getDefinitionName(Comment.fragments.comment)} - ...nest - } - hasNextPage - startCursor - endCursor - } - `, THREADING_LEVEL)} + ...CoralEmbedStream_Stream_comment } hasNextPage startCursor endCursor } } - ${Comment.fragments.comment} + ${commentFragment} `; const slots = [ diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js index 4a313d2b4..7b91d58ee 100644 --- a/client/coral-embed-stream/src/containers/StreamTabPanel.js +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -2,7 +2,7 @@ import React from 'react'; import StreamTabPanel from '../components/StreamTabPanel'; import {connect} from 'react-redux'; import omit from 'lodash/omit'; -import {getSlotComponents} from 'coral-framework/helpers/plugins'; +import {getSlotComponents, getSlotComponentProps} from 'coral-framework/helpers/plugins'; import {Tab, TabPane} from 'coral-ui'; import {getShallowChanges} from 'coral-framework/utils'; import isEqual from 'lodash/isEqual'; @@ -43,14 +43,14 @@ class StreamTabPanelContainer extends React.Component { } getSlotComponents(slot, props = this.props) { - return getSlotComponents(slot, props.reduxState, props.slotProps); + return getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData); } getPluginTabElements(props = this.props) { return this.getSlotComponents(props.tabSlot).map((PluginComponent) => ( @@ -61,7 +61,7 @@ class StreamTabPanelContainer extends React.Component { return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => ( )); @@ -95,6 +95,8 @@ StreamTabPanelContainer.propTypes = { fallbackTab: PropTypes.string.isRequired, tabSlot: PropTypes.string.isRequired, tabPaneSlot: PropTypes.string.isRequired, + slotProps: PropTypes.object.isRequired, + queryData: PropTypes.object, className: PropTypes.string, sub: PropTypes.bool, }; diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 0b0f4d325..838382978 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -2,11 +2,13 @@ import React from 'react'; import cn from 'classnames'; import styles from './Slot.css'; import {connect} from 'react-redux'; -import {getSlotElements} from 'coral-framework/helpers/plugins'; +import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/plugins'; import omit from 'lodash/omit'; import isEqual from 'lodash/isEqual'; import {getShallowChanges} from 'coral-framework/utils'; +const emptyConfig = {}; + class Slot extends React.Component { shouldComponentUpdate(next) { @@ -23,20 +25,20 @@ class Slot extends React.Component { return changes.length !== 0; } - getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, ...rest} = this.props) { + getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) { return rest; } getChildren(props = this.props) { - return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props)); + return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData); } render() { - const {inline = false, className, reduxState, defaultComponent: DefaultComponent} = this.props; + const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props; let children = this.getChildren(); - const pluginConfig = reduxState.config.pluginConfig || {}; + const pluginConfig = reduxState.config.pluginConfig || emptyConfig; if (children.length === 0 && DefaultComponent) { - children = ; + children = ; } return ( @@ -48,7 +50,8 @@ class Slot extends React.Component { } Slot.propTypes = { - fill: React.PropTypes.string.isRequired + fill: React.PropTypes.string.isRequired, + queryData: React.PropTypes.object, }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 7bebaa1f6..7f6e5f1c4 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -11,8 +11,11 @@ import camelize from './camelize'; import plugins from 'pluginsConfig'; import uuid from 'uuid/v4'; -export function getSlotComponents(slot, reduxState, props = {}) { - const pluginConfig = reduxState.config.plugin_config || {}; +// This is returned for pluginConfig when it is empty. +const emptyConfig = {}; + +export function getSlotComponents(slot, reduxState, props = {}, queryData = {}) { + const pluginConfig = reduxState.config.plugin_config || emptyConfig; return flatten(plugins // Filter out components that have slots and have been disabled in `plugin_config` @@ -25,7 +28,7 @@ export function getSlotComponents(slot, reduxState, props = {}) { if(!component.isExcluded) { return true; } - let resolvedProps = {...props, config: pluginConfig}; + let resolvedProps = getSlotComponentProps(component, reduxState, props, queryData); if (component.mapStateToProps) { resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; } @@ -33,17 +36,35 @@ export function getSlotComponents(slot, reduxState, props = {}) { }); } -export function isSlotEmpty(slot, reduxState, props) { - return getSlotComponents(slot, reduxState, props).length === 0; +export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { + return getSlotComponents(slot, reduxState, props, queryData).length === 0; +} + +/** + * getSlotComponentProps calculate the props we would pass to the slot component. + * query datas are only passed to the component if it is defined in `component.fragments`. + */ +export function getSlotComponentProps(component, reduxState, props, queryData) { + const pluginConfig = reduxState.config.plugin_config || emptyConfig; + return { + ...props, + config: pluginConfig, + ...( + component.fragments + ? pick(queryData, Object.keys(component.fragments)) + : queryData // TODO: should be {} + ) + }; } /** * Returns React Elements for given slot. */ -export function getSlotElements(slot, reduxState, props = {}) { - const pluginConfig = reduxState.config.plugin_config || {}; - return getSlotComponents(slot, reduxState, props) - .map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig})); +export function getSlotElements(slot, reduxState, props = {}, queryData = {}) { + return getSlotComponents(slot, reduxState, props, queryData) + .map((component, i) => { + return React.createElement(component, {key: i, ...getSlotComponentProps(component, reduxState, props, queryData)}); + }); } export function getSlotFragments(slot, part) { diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index c422f8705..aef7147d8 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,17 +1,98 @@ -// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38. - import React from 'react'; +import graphql from 'graphql-anywhere'; import {resolveFragments} from 'coral-framework/services/graphqlRegistry'; import mapValues from 'lodash/mapValues'; import hoistStatics from 'recompose/hoistStatics'; +import {getShallowChanges} from 'coral-framework/utils'; + +// TODO: Should not depend on `props.data` +// Currently necessary because of this https://github.com/apollographql/graphql-anywhere/issues/38 +function filter(doc, data, variables) { + const resolver = ( + fieldName, + root, + args, + context, + info, + ) => { + return root[info.resultKey]; + }; + + return graphql(resolver, doc, data, null, variables); +} + +// filterProps returns only the property as defined in the fragments. +// TODO: Should not depend on `props.data` +function filterProps(props, fragments) { + const filtered = {}; + Object.keys(fragments).forEach((key) => { + if (!(key in props)) { + return; + } + filtered[key] = filter(fragments[key], props[key], props.data.variables); + }); + return filtered; +} + +// hasEqualLeaves compares two different apollo query result for equality. +function hasEqualLeaves(a, b, path = '') { + for (const key in a) { + if (typeof a[key] === 'object') { + if (Array.isArray(a[key])) { + if (a[key].length !== b[key].length) { + return false; + } + } + if (!hasEqualLeaves(a[key], b[key], `${path}.${key}`)) { + return false; + } + continue; + } + if (a[key] !== b[key]) { + return false; + } + } + return true; +} export default (fragments) => hoistStatics((BaseComponent) => { class WithFragments extends React.Component { fragments = mapValues(fragments, (val) => resolveFragments(val)); + fragmentKeys = Object.keys(fragments).sort(); + + // Cache variables between lifecycles to speed up render. + filteredProps = filterProps(this.props, this.fragments) + queryDataHasChanged = false; + lastFilteredProps = null; + shallowChanges = null; + + componentWillReceiveProps(next) { + this.shallowChanges = getShallowChanges(this.props, next); + this.queryDataHasChanged = this.fragmentKeys.some((key) => this.shallowChanges.indexOf(key) >= 0); + + if (this.queryDataHasChanged) { + + // If query data has changed, we compute the next filtered props. + this.lastFilteredProps = this.filteredProps; + this.filteredProps = filterProps(next, this.fragments); + } + } + + shouldComponentUpdate(next) { + + // If only query data was changed. + if (this.queryDataHasChanged && this.shallowChanges.every((key) => this.fragmentKeys.indexOf(key) >= 0)) { + return !hasEqualLeaves(this.lastFilteredProps, this.filteredProps); + } + + return this.shallowChanges.length !== 0; + } render() { + const queryProps = this.filteredProps; return ; } } diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 9ac49cf43..0d118a12f 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -176,7 +176,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => { createdSubscription = context.client.subscribe({ query: REACTION_CREATED_SUBSCRIPTION, variables: { - assetId: this.props.root.asset.id, + assetId: this.props.asset.id, }, }).subscribe({ next: this.onReactionCreated, @@ -186,7 +186,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => { deletedSubscription = context.client.subscribe({ query: REACTION_DELETED_SUBSCRIPTION, variables: { - assetId: this.props.root.asset.id, + assetId: this.props.asset.id, }, }).subscribe({ next: this.onReactionDeleted, @@ -372,9 +372,16 @@ export default (reaction) => hoistStatics((WrappedComponent) => { const enhance = compose( withFragments({ + asset: gql` + fragment ${Reaction}Button_asset on Asset { + id + } + `, comment: gql` fragment ${Reaction}Button_comment on Comment { + id action_summaries { + __typename ... on ${Reaction}ActionSummary { count current_user { diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index 428c3f43f..b1d565557 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -93,8 +93,14 @@ export default (tag) => hoistStatics((WrappedComponent) => { const enhance = compose( withFragments({ + asset: gql` + fragment ${Tag}Button_asset on Asset { + id + } + `, comment: gql` fragment ${Tag}Button_comment on Comment { + id tags { tag { name diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 2195b0107..63ce30d01 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -16,8 +16,8 @@ class TabPaneContainer extends React.Component { query: LOAD_MORE_QUERY, variables: { limit: 5, - cursor: this.props.root.asset.featuredComments.endCursor, - asset_id: this.props.root.asset.id, + cursor: this.props.asset.featuredComments.endCursor, + asset_id: this.props.asset.id, sort: 'REVERSE_CHRONOLOGICAL', excludeIgnored: this.props.data.variables.excludeIgnored, }, From d228d49c7d0388bfbfa7800d4bd7826b44b2cdf3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 17 Aug 2017 22:44:13 +0700 Subject: [PATCH 054/109] Optimize rendering when `activeReplyBox` changes --- .../src/components/Comment.js | 31 ++++++++++++++++++- .../coral-embed-stream/src/graphql/utils.js | 6 +++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index cc1ee7bed..d34de96ac 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -16,13 +16,14 @@ import mapValues from 'lodash/mapValues'; import LoadMore from './LoadMore'; import {getEditableUntilDate} from './util'; +import {findCommentWithId} from '../graphql/utils'; import {TopRightMenu} from './TopRightMenu'; import CommentContent from './CommentContent'; import Slot from 'coral-framework/components/Slot'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import InactiveCommentLabel from './InactiveCommentLabel'; import {EditableCommentContent} from './EditableCommentContent'; -import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive} from 'coral-framework/utils'; +import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive, getShallowChanges} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; import CommentContainer from '../containers/Comment'; @@ -73,6 +74,17 @@ const ActionButton = ({children}) => { ); }; +// Determine whether the comment with id is in the part of the comments tree. +function containsCommentId(props, id) { + if (props.comment.id === id) { + return true; + } + if (props.comment.replies) { + return findCommentWithId(props.comment.replies.nodes, id); + } + return false; +} + export default class Comment extends React.Component { constructor(props) { @@ -112,6 +124,23 @@ export default class Comment extends React.Component { } } + shouldComponentUpdate(next) { + + // Specifically handle `activeReplyBox` if it is the only change. + const changes = getShallowChanges(this.props, next); + if (changes.length === 1 && changes[0] === 'activeReplyBox') { + if ( + !containsCommentId(next, this.props.activeReplyBox) && + !containsCommentId(next, next.activeReplyBox) + ) { + return false; + } + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + static propTypes = { // id of currently opened ReplyBox. tracked in Stream.js diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index ac6298616..11d3d411b 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -117,7 +117,7 @@ export function getTopLevelParent(comment) { return comment; } -function findComment(nodes, callback) { +export function findComment(nodes, callback) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (callback(node)) { @@ -133,6 +133,10 @@ function findComment(nodes, callback) { return false; } +export function findCommentWithId(nodes, id) { + return findComment(nodes, (node) => node.id === id); +} + export function findCommentInEmbedQuery(root, callbackOrId) { let callback = callbackOrId; if (typeof callbackOrId === 'string') { From abb8b775b92c78cfcaf95ed1ca79fce2fe4937f3 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Aug 2017 12:44:20 -0300 Subject: [PATCH 055/109] working banned and suspended words --- client/coral-admin/src/reducers/settings.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js index 6e29ba719..336047e5b 100644 --- a/client/coral-admin/src/reducers/settings.js +++ b/client/coral-admin/src/reducers/settings.js @@ -74,8 +74,10 @@ export default function settings (state = initialState, action) { }; case actions.WORDLIST_UPDATED: return update(state, { - wordList: { - [action.listName]: {$set: action.list}, + wordlist: { + [action.listName]: { + $set: action.list + } } }); case actions.DOMAINLIST_UPDATED: From ef815005a87f1a41526e0fd350daaca50bb9e3c5 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Aug 2017 13:12:07 -0300 Subject: [PATCH 056/109] =?UTF-8?q?=C3=9Apdated=20suspected=20words?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CommentBodyHighlighter.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/components/CommentBodyHighlighter.js b/client/coral-admin/src/components/CommentBodyHighlighter.js index 3b3ee3318..39be90d81 100644 --- a/client/coral-admin/src/components/CommentBodyHighlighter.js +++ b/client/coral-admin/src/components/CommentBodyHighlighter.js @@ -8,14 +8,11 @@ export default ({suspectWords, bannedWords, body, ...rest}) => { const links = linkify.getMatches(body); const linkText = links ? links.map((link) => link.raw) : []; - // since words are checked against word boundaries on the backend, - // should be the behavior on the front end as well. - // currently the highlighter plugin does not support out of the box. - const searchWords = [...suspectWords, ...bannedWords] - .filter((w) => { - return new RegExp(`(^|\\s)${w}(\\s|$)`, 'i').test(body); - }) - .concat(linkText); + const searchWords = [ + ...suspectWords, + ...bannedWords, + ...linkText + ]; return ( Date: Thu, 17 Aug 2017 23:22:30 +0700 Subject: [PATCH 057/109] Keep old query data when not changed in withFragments --- client/coral-framework/hocs/withFragments.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index aef7147d8..76a6f7035 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -63,18 +63,19 @@ export default (fragments) => hoistStatics((BaseComponent) => { // Cache variables between lifecycles to speed up render. filteredProps = filterProps(this.props, this.fragments) queryDataHasChanged = false; - lastFilteredProps = null; shallowChanges = null; componentWillReceiveProps(next) { this.shallowChanges = getShallowChanges(this.props, next); - this.queryDataHasChanged = this.fragmentKeys.some((key) => this.shallowChanges.indexOf(key) >= 0); - if (this.queryDataHasChanged) { + if (this.fragmentKeys.some((key) => this.shallowChanges.indexOf(key) >= 0)) { + const nextFilteredProps = filterProps(next, this.fragments); + this.queryDataHasChanged = !hasEqualLeaves(this.filteredProps, nextFilteredProps); + if (this.queryDataHasChanged) { - // If query data has changed, we compute the next filtered props. - this.lastFilteredProps = this.filteredProps; - this.filteredProps = filterProps(next, this.fragments); + // Only changed props when query data has changed. + this.filteredProps = filterProps(next, this.fragments); + } } } @@ -82,7 +83,7 @@ export default (fragments) => hoistStatics((BaseComponent) => { // If only query data was changed. if (this.queryDataHasChanged && this.shallowChanges.every((key) => this.fragmentKeys.indexOf(key) >= 0)) { - return !hasEqualLeaves(this.lastFilteredProps, this.filteredProps); + return this.queryDataHasChanged; } return this.shallowChanges.length !== 0; From af4b01c2aee7a91a0b250c5902b78d99214fc486 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 17 Aug 2017 23:23:18 +0700 Subject: [PATCH 058/109] Correctly handly activeReplyBox optimization --- 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 d34de96ac..dcc52d85a 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -130,7 +130,7 @@ export default class Comment extends React.Component { const changes = getShallowChanges(this.props, next); if (changes.length === 1 && changes[0] === 'activeReplyBox') { if ( - !containsCommentId(next, this.props.activeReplyBox) && + !containsCommentId(this.props, this.props.activeReplyBox) && !containsCommentId(next, next.activeReplyBox) ) { return false; From 0808f97fb59c5c30537b98494c07b60eb61dcaeb Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 16:40:42 +0700 Subject: [PATCH 059/109] Consider state when optimizing comment --- client/coral-embed-stream/src/components/Comment.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index dcc52d85a..5a2572574 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -124,14 +124,14 @@ export default class Comment extends React.Component { } } - shouldComponentUpdate(next) { + shouldComponentUpdate(nextProps, nextState) { // Specifically handle `activeReplyBox` if it is the only change. - const changes = getShallowChanges(this.props, next); + const changes = [...getShallowChanges(this.props, nextProps), ...getShallowChanges(this.state, nextState)]; if (changes.length === 1 && changes[0] === 'activeReplyBox') { if ( !containsCommentId(this.props, this.props.activeReplyBox) && - !containsCommentId(next, next.activeReplyBox) + !containsCommentId(nextProps, nextProps.activeReplyBox) ) { return false; } From fabc8b8c46ce8ac66f27e02890a8913edaf71678 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 17:49:57 +0700 Subject: [PATCH 060/109] Check when one of the leaves are empty --- client/coral-framework/hocs/withFragments.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index 76a6f7035..33c39abd0 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -4,6 +4,7 @@ import {resolveFragments} from 'coral-framework/services/graphqlRegistry'; import mapValues from 'lodash/mapValues'; import hoistStatics from 'recompose/hoistStatics'; import {getShallowChanges} from 'coral-framework/utils'; +import union from 'lodash/union'; // TODO: Should not depend on `props.data` // Currently necessary because of this https://github.com/apollographql/graphql-anywhere/issues/38 @@ -36,8 +37,11 @@ function filterProps(props, fragments) { // hasEqualLeaves compares two different apollo query result for equality. function hasEqualLeaves(a, b, path = '') { - for (const key in a) { - if (typeof a[key] === 'object') { + for (const key of union(Object.keys(a), Object.keys(b))) { + if (!(key in a) || !(key in b)) { + return false; + } + if (typeof a[key] === 'object' && a[key] && b[key]) { if (Array.isArray(a[key])) { if (a[key].length !== b[key].length) { return false; From 6a5f6e3ce1745ca7ca8b793ba940b3fbb7fd8979 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 17:57:32 +0700 Subject: [PATCH 061/109] Adapt loading detection --- client/coral-admin/src/containers/UserDetail.js | 2 +- client/coral-embed-stream/src/containers/Stream.js | 6 +++++- client/coral-settings/containers/ProfileContainer.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 2ae6e4c9c..88c43ce5f 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -108,7 +108,7 @@ class UserDetailContainer extends React.Component { return null; } - const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0; + const loading = this.props.data.loading; return ; } return = 0; + const loading = this.props.data.loading; if (!auth.loggedIn) { return ; From 5e13af82824d4196ba7d5a3715415fa6245c805c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 18:42:41 +0700 Subject: [PATCH 062/109] Fix emit showMoreReplies event --- client/coral-embed-stream/src/components/Comment.js | 10 ++++++++-- client/coral-embed-stream/src/components/Stream.js | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6c231f4ae..df29ff6ff 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -180,6 +180,9 @@ export default class Comment extends React.Component { // edit a comment, passed (id, asset_id, { body }) editComment: PropTypes.func, + + // emit custom events + emit: PropTypes.func.isRequired, } editComment = (...args) => { @@ -207,7 +210,7 @@ export default class Comment extends React.Component { } loadNewReplies = () => { - const {replies, replyCount, id} = this.props.comment; + const {comment: {replies, replyCount, id}, emit} = this.props; if (replyCount > replies.nodes.length) { this.setState({loadingState: 'loading'}); this.props.loadMore(id) @@ -221,10 +224,11 @@ export default class Comment extends React.Component { this.setState({loadingState: 'error'}); forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); }); + emit('ui.Comment.showMoreReplies', {id}); return; } this.setState(resetCursors); - this.props.emit('ui.Comment.showMoreReplies'); + emit('ui.Comment.showMoreReplies', {id}); }; showReplyBox = () => { @@ -315,6 +319,7 @@ export default class Comment extends React.Component { showSignInDialog, liveUpdates, commentIsIgnored, + emit, commentClassNames = [] } = this.props; @@ -568,6 +573,7 @@ export default class Comment extends React.Component { reactKey={reply.id} key={reply.id} comment={reply} + emit={emit} />; })} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 7b9e5f566..8fe373859 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -99,7 +99,8 @@ class Stream extends React.Component { loadMoreComments, viewAllComments, auth: {loggedIn, user}, - editName + editName, + emit, } = this.props; const {keepCommentBox} = this.state; const open = !asset.isClosed; @@ -234,6 +235,7 @@ class Stream extends React.Component { charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} + emit={emit} liveUpdates={true} />
@@ -292,7 +294,7 @@ class Stream extends React.Component { charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} - emit={this.props.emit} + emit={emit} /> From 8eaac3eaa20aadab7ccc5fd1535777453f7723fe Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 19:50:56 +0700 Subject: [PATCH 063/109] Tiny refactor.. --- .../containers/ProfileContainer.js | 5 +- client/talk-plugin-history/Comment.js | 83 ++++++++++--------- client/talk-plugin-history/CommentHistory.js | 6 +- 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 3423cb6e2..6ffad3c87 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -51,7 +51,7 @@ class ProfileContainer extends Component { }; render() { - const {auth, auth: {user}, asset, showSignInDialog, stopIgnoringUser} = this.props; + const {auth, auth: {user}, showSignInDialog, stopIgnoringUser, root, data} = this.props; const {me} = this.props.root; const loading = this.props.data.loading; @@ -87,7 +87,7 @@ class ProfileContainer extends Component {

{t('framework.my_comments')}

{me.comments.nodes.length - ? + ? :

{t('user_no_comment')}

}
); @@ -138,7 +138,6 @@ const withProfileQuery = withQuery( `); const mapStateToProps = (state) => ({ - asset: state.asset, auth: state.auth }); diff --git a/client/talk-plugin-history/Comment.js b/client/talk-plugin-history/Comment.js index f90362e2d..d1640cb5e 100644 --- a/client/talk-plugin-history/Comment.js +++ b/client/talk-plugin-history/Comment.js @@ -7,54 +7,57 @@ import CommentContent from '../coral-embed-stream/src/components/CommentContent' import t from 'coral-framework/services/i18n'; -const Comment = (props) => { - return ( - - - ); -}; + ); + } +} Comment.propTypes = { comment: PropTypes.shape({ id: PropTypes.string, body: PropTypes.string }).isRequired, - asset: PropTypes.shape({ - url: PropTypes.string, - title: PropTypes.string - }).isRequired }; export default Comment; diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js index d584d6a0a..43ae93f1d 100644 --- a/client/talk-plugin-history/CommentHistory.js +++ b/client/talk-plugin-history/CommentHistory.js @@ -22,16 +22,18 @@ class CommentHistory extends React.Component { } render() { - const {link, comments} = this.props; + const {link, comments, data, root} = this.props; return (
{comments.nodes.map((comment, i) => { return ; + />; })}
{comments.hasNextPage && From dc49d23dc8d12776d1a51f98b6568d297be1a60f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 19:55:51 +0700 Subject: [PATCH 064/109] Define fragments for PermalinkButton --- .../client/containers/PermalinkButton.js | 16 ++++++++++++++++ plugins/talk-plugin-permalink/client/index.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 plugins/talk-plugin-permalink/client/containers/PermalinkButton.js diff --git a/plugins/talk-plugin-permalink/client/containers/PermalinkButton.js b/plugins/talk-plugin-permalink/client/containers/PermalinkButton.js new file mode 100644 index 000000000..5e1d11110 --- /dev/null +++ b/plugins/talk-plugin-permalink/client/containers/PermalinkButton.js @@ -0,0 +1,16 @@ +import {gql} from 'react-apollo'; +import PermalinkButton from '../components/PermalinkButton'; +import {withFragments} from 'plugin-api/beta/client/hocs'; + +export default withFragments({ + asset: gql` + fragment TalkPermalink_Button_asset on Asset { + url + } + `, + comment: gql` + fragment TalkPermalink_Button_comment on Comment { + id + } + ` +})(PermalinkButton); diff --git a/plugins/talk-plugin-permalink/client/index.js b/plugins/talk-plugin-permalink/client/index.js index d413da870..c28c4ae71 100644 --- a/plugins/talk-plugin-permalink/client/index.js +++ b/plugins/talk-plugin-permalink/client/index.js @@ -1,4 +1,4 @@ -import PermalinkButton from './components/PermalinkButton'; +import PermalinkButton from './containers/PermalinkButton'; export default { slots: { From 1dcdccecbc1f3699e055db6290b3d90db2caf0db Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 18 Aug 2017 10:19:10 -0300 Subject: [PATCH 065/109] UserDetail UI redone and Flagging Reliability added --- .../coral-admin/src/components/UserDetail.css | 100 ++++++++++++++---- .../coral-admin/src/components/UserDetail.js | 65 +++++++----- client/coral-ui/components/Icon.css | 2 + 3 files changed, 118 insertions(+), 49 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.css b/client/coral-admin/src/components/UserDetail.css index 20e090f47..5cbe0fe32 100644 --- a/client/coral-admin/src/components/UserDetail.css +++ b/client/coral-admin/src/components/UserDetail.css @@ -1,6 +1,78 @@ .copyButton { - float: right; - top: -10px; + background-color: white; + border: solid 1px; + padding: 2px 6px; + height: auto; + line-height: initial; + min-width: auto; + letter-spacing: normal; + font-size: 0.9em; + margin-left: 10px; +} + +.userDetailList { + list-style: none; + padding: 0; + margin: 0; +} + +.userDetailItem { + margin: 0 5px; + font-weight: 500; +} + +.stats { + display: flex; + list-style: none; + padding: 0; + margin: 0; + text-align: center; + margin: 15px 0 5px; + color: #595959; +} + +.stat { + margin-right: 20px; +} + +.stat:last-child { + margin-right: 0px; +} + +.statItem, .statReportResult { + padding: 3px 5px; + background-color: #D8D8D8; + border-radius: 3px; + font-weight: 500; + display: block; + font-size: 0.9em; + line-height: normal; + letter-spacing: 0.4px; + min-width: 60px; +} + +.statResult { + font-size: 1.5em; + padding: 5px 0; + display: inline-block; +} + +.statReportResult { + color: white; + margin: 5px 0; + font-weight: 400; +} + +.statReportResult.reliable { + background-color: #749C48; +} + +.statReportResult.neutral { + background-color: #616161; +} + +.statReportResult.unreliable { + background-color: #F44336; } .memberSince { @@ -8,27 +80,9 @@ } .small { - color: #aaa; -} - -.stats { - display: flex; - - .stat { - margin: 0 4px 10px 0px; - } - - .stat:last-child { - margin-right: 0; - } - - p { - margin: 0; - } - - .stat p:first-child { - font-weight: bold; - } + color: #888888; + font-size: 0.9em; + letter-spacing: 0.4px; } .profileEmail { diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 34773ddef..64018cde8 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -1,12 +1,14 @@ -import React, {PropTypes} from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; import Comment from './UserDetailComment'; import styles from './UserDetail.css'; -import {Button, Drawer, Spinner} from 'coral-ui'; +import {Icon, Button, Drawer, Spinner} from 'coral-ui'; import {Slot} from 'coral-framework/components'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; import {actionsMap} from '../utils/moderationQueueActionsMap'; import ClickOutside from 'coral-framework/components/ClickOutside'; import LoadMore from '../components/LoadMore'; +import cn from 'classnames'; export default class UserDetail extends React.Component { @@ -74,13 +76,6 @@ export default class UserDetail extends React.Component { loadMore, } = this.props; - const localProfile = user.profiles.find((p) => p.provider === 'local'); - - let profile; - if (localProfile) { - profile = localProfile.id; - } - let rejectedPercent = (rejectedComments / totalComments) * 100; if (rejectedPercent === Infinity || isNaN(rejectedPercent)) { @@ -94,8 +89,40 @@ export default class UserDetail extends React.Component {

{user.username}

- {profile && this.profile = ref} value={profile} />} - +
    +
  • + + Member Since: + {new Date(user.created_at).toLocaleString()} +
  • + + {user.profiles.map(({id}) => +
  • + + Email: + {id} +
  • + )} +
+ +
    +
  • + Total Comments + {totalComments} +
  • +
  • + Reject Rate + {`${(rejectedPercent).toFixed(1)}%`} +
  • +
  • + Reports + Reliable +
  • +
+ +

+ Data represents the last six months of activity +

-

Member since {new Date(user.created_at).toLocaleString()}

+
-

- Account summary -
Data represents the last six months of activity -

-
-
-

Total Comments

-

{totalComments}

-
-
-

Reject Rate

-

{`${(rejectedPercent).toFixed(1)}%`}

-
-
{ selectedCommentIds.length === 0 ? ( diff --git a/client/coral-ui/components/Icon.css b/client/coral-ui/components/Icon.css index 16fe6d235..1a118257a 100644 --- a/client/coral-ui/components/Icon.css +++ b/client/coral-ui/components/Icon.css @@ -1,3 +1,5 @@ .root { + vertical-align: middle; + font-size: inherit; } From 9f62754fd77f8fcdcdc99d4cc97b9add8011850f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 20:33:37 +0700 Subject: [PATCH 066/109] Show warnings when slot components uses query data without fragments --- client/coral-framework/helpers/plugins.js | 32 ++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 7f6e5f1c4..8fc3310e6 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -5,8 +5,10 @@ import merge from 'lodash/merge'; import flattenDeep from 'lodash/flattenDeep'; import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; +import mapValues from 'lodash/mapValues'; import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers} from 'coral-framework/services/store'; +import {getDisplayName} from 'coral-framework/helpers/hoc'; import camelize from './camelize'; import plugins from 'pluginsConfig'; import uuid from 'uuid/v4'; @@ -40,6 +42,34 @@ export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { return getSlotComponents(slot, reduxState, props, queryData).length === 0; } +// Memoize the warnings so we only show them once. +const memoizedWarnings = []; + +function withWarnings(component, queryData) { + if (process.env.NODE_ENV !== 'production') { + + // Show warnings when accessing queryData only when not in production. + return mapValues(queryData, (value, key) => { + return new Proxy(queryData[key], { + get(target, name) { + + // Only care about the components defined in the plugins. + if (component.talkPluginName) { + const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; + if (memoizedWarnings.indexOf(warning) === -1) { + console.warn(warning); + memoizedWarnings.push(warning); + } + } + return queryData[key][name]; + } + }); + }); + } + + return queryData; +} + /** * getSlotComponentProps calculate the props we would pass to the slot component. * query datas are only passed to the component if it is defined in `component.fragments`. @@ -52,7 +82,7 @@ export function getSlotComponentProps(component, reduxState, props, queryData) { ...( component.fragments ? pick(queryData, Object.keys(component.fragments)) - : queryData // TODO: should be {} + : withWarnings(component, queryData) ) }; } From 00f365ab38d7d8dfd1c689add5e3d7bf552c0623 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 20:40:51 +0700 Subject: [PATCH 067/109] Add missing fragments --- .../client/containers/Tag.js | 15 +++++++++++++++ .../talk-plugin-featured-comments/client/index.js | 2 +- .../client/containers/OffTopicTag.js | 15 +++++++++++++++ plugins/talk-plugin-offtopic/client/index.js | 2 +- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 plugins/talk-plugin-featured-comments/client/containers/Tag.js create mode 100644 plugins/talk-plugin-offtopic/client/containers/OffTopicTag.js diff --git a/plugins/talk-plugin-featured-comments/client/containers/Tag.js b/plugins/talk-plugin-featured-comments/client/containers/Tag.js new file mode 100644 index 000000000..60d9e1e2b --- /dev/null +++ b/plugins/talk-plugin-featured-comments/client/containers/Tag.js @@ -0,0 +1,15 @@ +import {gql} from 'react-apollo'; +import Tag from '../components/Tag'; +import {withFragments} from 'plugin-api/beta/client/hocs'; + +export default withFragments({ + comment: gql` + fragment TalkFeaturedComments_Tag_comment on Comment { + tags { + tag { + name + } + } + } + ` +})(Tag); diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js index ad10f87ca..79c3d874f 100644 --- a/plugins/talk-plugin-featured-comments/client/index.js +++ b/plugins/talk-plugin-featured-comments/client/index.js @@ -1,5 +1,5 @@ import Tab from './containers/Tab'; -import Tag from './components/Tag'; +import Tag from './containers/Tag'; import Button from './components/Button'; import TabPane from './containers/TabPane'; import translations from './translations.yml'; diff --git a/plugins/talk-plugin-offtopic/client/containers/OffTopicTag.js b/plugins/talk-plugin-offtopic/client/containers/OffTopicTag.js new file mode 100644 index 000000000..bf57dc5cb --- /dev/null +++ b/plugins/talk-plugin-offtopic/client/containers/OffTopicTag.js @@ -0,0 +1,15 @@ +import {gql} from 'react-apollo'; +import OffTopicTag from '../components/OffTopicTag'; +import {withFragments} from 'plugin-api/beta/client/hocs'; + +export default withFragments({ + comment: gql` + fragment TalkOfftopic_OffTopicTag_comment on Comment { + tags { + tag { + name + } + } + } + ` +})(OffTopicTag); diff --git a/plugins/talk-plugin-offtopic/client/index.js b/plugins/talk-plugin-offtopic/client/index.js index b284f7280..24bfc095f 100644 --- a/plugins/talk-plugin-offtopic/client/index.js +++ b/plugins/talk-plugin-offtopic/client/index.js @@ -1,5 +1,5 @@ import translations from './translations.json'; -import OffTopicTag from './components/OffTopicTag'; +import OffTopicTag from './containers/OffTopicTag'; import OffTopicFilter from './containers/OffTopicFilter'; import OffTopicCheckbox from './containers/OffTopicCheckbox'; import reducer from './reducer'; From de90bdbd19c4bc4840d79f29946b59c7ef8ba25e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 21:19:13 +0700 Subject: [PATCH 068/109] Gracefully handle when data is not available --- client/coral-framework/hocs/withFragments.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index 33c39abd0..e6aca318a 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -30,7 +30,9 @@ function filterProps(props, fragments) { if (!(key in props)) { return; } - filtered[key] = filter(fragments[key], props[key], props.data.variables); + filtered[key] = props.data + ? filter(fragments[key], props[key], props.data.variables) + : props[key]; }); return filtered; } From e5156f7fc1895ef826ae58d7651a218bb58d254a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 21:36:14 +0700 Subject: [PATCH 069/109] Integrate embed slot in to graphql framework --- client/coral-embed-stream/src/components/Embed.js | 10 +++++++--- client/coral-embed-stream/src/containers/Embed.js | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index f918b7a16..a74028627 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -28,7 +28,7 @@ export default class Embed extends React.Component { }; render() { - const {activeTab, commentId, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props; + const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props; const {user} = this.props.auth; const hasHighlightedComment = !!commentId; @@ -64,14 +64,18 @@ export default class Embed extends React.Component { } - + - + diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index e21a683d1..731cab264 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -11,7 +11,7 @@ import {Spinner} from 'coral-ui'; import * as authActions from 'coral-framework/actions/auth'; import * as assetActions from 'coral-framework/actions/asset'; import pym from 'coral-framework/services/pym'; -import {getDefinitionName} from 'coral-framework/utils'; +import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from './Stream'; @@ -146,12 +146,17 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql` } `; +const slots = [ + 'embed', +]; + const EMBED_QUERY = gql` query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) { me { id status } + ${getSlotFragmentSpreads(slots, 'root')} ...${getDefinitionName(Stream.fragments.root)} } ${Stream.fragments.root} From 64580696fb8d620e7c084cf6b9604296ce68bfd9 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 18 Aug 2017 11:42:23 -0300 Subject: [PATCH 070/109] Reliability Added --- client/coral-admin/src/components/UserDetail.js | 6 +++++- client/coral-admin/src/containers/UserDetail.js | 3 +++ client/coral-framework/utils/user.js | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 client/coral-framework/utils/user.js diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 64018cde8..6af1b6678 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -9,6 +9,8 @@ import {actionsMap} from '../utils/moderationQueueActionsMap'; import ClickOutside from 'coral-framework/components/ClickOutside'; import LoadMore from '../components/LoadMore'; import cn from 'classnames'; +import capitalize from 'lodash/capitalize'; +import {getReliability} from 'coral-framework/utils/user'; export default class UserDetail extends React.Component { @@ -116,7 +118,9 @@ export default class UserDetail extends React.Component {
  • Reports - Reliable + + {capitalize(getReliability(user.reliable.flagger))} +
  • diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 2ae6e4c9c..41fccb7c5 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -142,6 +142,9 @@ export const withUserDetailQuery = withQuery(gql` id provider } + reliable { + flagger + } ${getSlotFragmentSpreads(slots, 'user')} } totalComments: commentCount(query: {author_id: $author_id}) diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js new file mode 100644 index 000000000..044583026 --- /dev/null +++ b/client/coral-framework/utils/user.js @@ -0,0 +1,14 @@ + /** + * getReliability + * retrieves reliability value as string + */ + +export const getReliability = (reliabilityValue) => { + if (reliabilityValue === null) { + return 'neutral'; + } else if (reliabilityValue) { + return 'reliable'; + } else { + return 'unreliable'; + } +}; From 833e78f99a6a40cef12183c3a823071532a5cc83 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 21:41:46 +0700 Subject: [PATCH 071/109] Integrate ProfileContainer slot into graphql framework --- client/coral-settings/containers/ProfileContainer.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 6ffad3c87..f1b3787dd 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -14,6 +14,7 @@ import CommentHistory from 'talk-plugin-history/CommentHistory'; import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth'; import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; @@ -94,6 +95,11 @@ class ProfileContainer extends Component { } } +// TODO: This Slot should be included in `talk-plugin-history` instead. +const slots = [ + 'commentContent', +]; + const CommentFragment = gql` fragment TalkSettings_CommentConnectionFragment on CommentConnection { nodes { @@ -103,8 +109,10 @@ const CommentFragment = gql` id title url + ${getSlotFragmentSpreads(slots, 'asset')} } created_at + ${getSlotFragmentSpreads(slots, 'comment')} } endCursor hasNextPage @@ -133,6 +141,7 @@ const withProfileQuery = withQuery( ...TalkSettings_CommentConnectionFragment } } + ${getSlotFragmentSpreads(slots, 'root')} } ${CommentFragment} `); From 0cbf6e24f77c64b56208f2313cabe7c93445a6b3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 21:59:43 +0700 Subject: [PATCH 072/109] Handle null values --- client/coral-framework/helpers/plugins.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 8fc3310e6..57caf2c46 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -50,6 +50,11 @@ function withWarnings(component, queryData) { // Show warnings when accessing queryData only when not in production. return mapValues(queryData, (value, key) => { + + // Keep null values.. + if (!queryData[key]) { + return queryData[key]; + } return new Proxy(queryData[key], { get(target, name) { From b8452b535bfdc3fb486d759d3d273abd1e2ac8e6 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 22:19:03 +0700 Subject: [PATCH 073/109] Allow passing custom fragments to withReaction and withTag --- plugin-api/beta/client/hocs/withReaction.js | 20 +++++++++++++++---- plugin-api/beta/client/hocs/withTags.js | 20 +++++++++++++++---- .../client/containers/Comment.js | 1 - .../client/containers/ModTag.js | 13 +++++++++++- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 0d118a12f..158431898 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -13,13 +13,17 @@ import {capitalize} from 'coral-framework/helpers/strings'; import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; import * as PropTypes from 'prop-types'; +import {getDefinitionName} from '../utils'; -export default (reaction) => hoistStatics((WrappedComponent) => { +export default (reaction, options = {}) => hoistStatics((WrappedComponent) => { if (typeof reaction !== 'string') { console.error('Reaction must be a valid string'); return null; } + // fragments allow the extension of the fragments defined in this HOC. + const {fragments = {}} = options; + // Global instance counter for each `reaction` type. let instances = 0; @@ -248,7 +252,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => { } render() { - const {comment} = this.props; + const {root, asset, comment} = this.props; const reactionSummary = getMyActionSummary( `${Reaction}ActionSummary`, @@ -263,10 +267,12 @@ export default (reaction) => hoistStatics((WrappedComponent) => { const alreadyReacted = !!reactionSummary; return hoistStatics((WrappedComponent) => { const enhance = compose( withFragments({ + ...fragments, asset: gql` fragment ${Reaction}Button_asset on Asset { id + ${fragments.asset ? `...${getDefinitionName(fragments.asset)}` : ''} } + ${fragments.asset ? fragments.asset : ''} `, comment: gql` fragment ${Reaction}Button_comment on Comment { @@ -389,7 +398,10 @@ export default (reaction) => hoistStatics((WrappedComponent) => { } } } - }` + ${fragments.comment ? `...${getDefinitionName(fragments.comment)}` : ''} + } + ${fragments.comment ? fragments.comment : ''} + ` }), connect(mapStateToProps, mapDispatchToProps), withDeleteReaction, diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index b1d565557..d13fbc8c2 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -9,13 +9,17 @@ import withFragments from 'coral-framework/hocs/withFragments'; import {addNotification} from 'coral-framework/actions/notification'; import {forEachError, isTagged} from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; +import {getDefinitionName} from '../utils'; -export default (tag) => hoistStatics((WrappedComponent) => { +export default (tag, options = {}) => hoistStatics((WrappedComponent) => { if (typeof tag !== 'string') { console.error('Tag must be a valid string'); return null; } + // fragments allow the extension of the fragments defined in this HOC. + const {fragments = {}} = options; + const Tag = capitalize(tag); const TAG = tag.toUpperCase(); @@ -69,13 +73,15 @@ export default (tag) => hoistStatics((WrappedComponent) => { } render() { - const {comment, user, config} = this.props; + const {root, asset, comment, user, config} = this.props; const alreadyTagged = isTagged(comment.tags, TAG); return hoistStatics((WrappedComponent) => { const enhance = compose( withFragments({ + ...fragments, asset: gql` fragment ${Tag}Button_asset on Asset { id + ${fragments.asset ? `...${getDefinitionName(fragments.asset)}` : ''} } + ${fragments.asset ? fragments.asset : ''} `, comment: gql` fragment ${Tag}Button_comment on Comment { @@ -106,7 +115,10 @@ export default (tag) => hoistStatics((WrappedComponent) => { name } } - }` + ${fragments.comment ? `...${getDefinitionName(fragments.comment)}` : ''} + } + ${fragments.comment ? fragments.comment : ''} + ` }), connect(mapStateToProps, mapDispatchToProps), withAddTag, diff --git a/plugins/talk-plugin-featured-comments/client/containers/Comment.js b/plugins/talk-plugin-featured-comments/client/containers/Comment.js index 7080627e6..bba764a1d 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/containers/Comment.js @@ -31,7 +31,6 @@ export default withFragments({ name } } - user { id username diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js index 3b564d127..7081c7ec6 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js @@ -1,5 +1,16 @@ import ModTag from '../components/ModTag'; import {withTags} from 'plugin-api/beta/client/hocs'; +import {gql} from 'react-apollo'; -export default withTags('featured')(ModTag); +const fragments = { + comment: gql` + fragment TalkFeaturedComments_ModTag_comment on Comment { + user { + username + } + } + ` +}; + +export default withTags('featured', {fragments})(ModTag); From 0a602b79c3a1efeeecc0f15a76b04d03e2806082 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 22:21:51 +0700 Subject: [PATCH 074/109] Enable optimization for more slots --- .../coral-admin/src/components/UserDetail.js | 4 +- .../routes/Moderation/components/Comment.js | 42 ++++++++++--------- .../Moderation/components/Moderation.js | 3 +- client/talk-plugin-history/Comment.js | 4 +- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 34773ddef..afdccb634 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -56,6 +56,7 @@ export default class UserDetail extends React.Component { renderLoaded() { const { + root, root: { user, totalComments, @@ -101,8 +102,7 @@ export default class UserDetail extends React.Component {

    Member since {new Date(user.created_at).toLocaleString()}


    diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index c6ec0fd4c..60077e80a 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -29,7 +29,12 @@ class Comment extends React.Component { bannedWords, selected, className, - ...props + data, + root, + currentUserId, + currentAsset, + acceptComment, + rejectComment, } = this.props; const flagActionSummaries = getActionSummary('FlagActionSummary', comment); @@ -38,20 +43,22 @@ class Comment extends React.Component { let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; - const showSuspenUserDialog = () => props.showSuspendUserDialog({ + const showSuspenUserDialog = () => this.props.showSuspendUserDialog({ userId: comment.user.id, username: comment.user.username, commentId: comment.id, commentStatus: comment.status, }); - const showBanUserDialog = () => props.showBanUserDialog({ + const showBanUserDialog = () => this.props.showBanUserDialog({ userId: comment.user.id, username: comment.user.username, commentId: comment.id, commentStatus: comment.status, }); + const queryData = {root, comment, asset: comment.asset}; + return (
  •  ({t('comment.edited')}) : null } - {props.currentUserId !== comment.user.id && + {currentUserId !== comment.user.id &&
  • @@ -103,7 +108,7 @@ class Comment extends React.Component {
    Story: {comment.asset.title} - {!props.currentAsset && + {!currentAsset && {t('modqueue.moderate')}}
    @@ -124,10 +129,9 @@ class Comment extends React.Component {

    @@ -150,30 +154,28 @@ class Comment extends React.Component { acceptComment={() => (comment.status === 'ACCEPTED' ? null - : props.acceptComment({commentId: comment.id}))} + : acceptComment({commentId: comment.id}))} rejectComment={() => (comment.status === 'REJECTED' ? null - : props.rejectComment({commentId: comment.id}))} + : rejectComment({commentId: comment.id}))} /> ); })}
    {flagActions && flagActions.length ?

    Date: Fri, 18 Aug 2017 22:35:13 +0700 Subject: [PATCH 075/109] Only use Proxy when available --- client/coral-framework/helpers/plugins.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 57caf2c46..3173115c2 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -45,8 +45,10 @@ export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { // Memoize the warnings so we only show them once. const memoizedWarnings = []; +// withWarnings decorates the props of queryData with a proxy that +// prints a warning when accessing deeper props. function withWarnings(component, queryData) { - if (process.env.NODE_ENV !== 'production') { + if (process.env.NODE_ENV !== 'production' && window.Proxy) { // Show warnings when accessing queryData only when not in production. return mapValues(queryData, (value, key) => { From e77a8ce3c7bdda5ac2bdadf33ef345acf4ac6fae Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 22:36:13 +0700 Subject: [PATCH 076/109] More comments --- client/coral-framework/components/Slot.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 838382978..c0cca143c 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -51,6 +51,8 @@ class Slot extends React.Component { Slot.propTypes = { fill: React.PropTypes.string.isRequired, + + // props coming from graphql must be passed through this property. queryData: React.PropTypes.object, }; From 44ec469fa5c146fed1e819627ce23ef06f0c06ff Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 22:47:30 +0700 Subject: [PATCH 077/109] Disable fragment warnings --- plugin-api/beta/client/hocs/withReaction.js | 10 ++++++++++ plugin-api/beta/client/hocs/withTags.js | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 158431898..3300ac2d3 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -15,6 +15,16 @@ import hoistStatics from 'recompose/hoistStatics'; import * as PropTypes from 'prop-types'; import {getDefinitionName} from '../utils'; +/* + * Disable false-positive warning below, as it doesn't work well with how we currently + * assemble the queries. + * + * Warning: fragment with name {fragment name} already exists. + * graphql-tag enforces all fragment names across your application to be unique; read more about + * this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names + */ +gql.disableFragmentWarnings(); + export default (reaction, options = {}) => hoistStatics((WrappedComponent) => { if (typeof reaction !== 'string') { console.error('Reaction must be a valid string'); diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index d13fbc8c2..491c35038 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -11,6 +11,16 @@ import {forEachError, isTagged} from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; import {getDefinitionName} from '../utils'; +/* + * Disable false-positive warning below, as it doesn't work well with how we currently + * assemble the queries. + * + * Warning: fragment with name {fragment name} already exists. + * graphql-tag enforces all fragment names across your application to be unique; read more about + * this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names + */ +gql.disableFragmentWarnings(); + export default (tag, options = {}) => hoistStatics((WrappedComponent) => { if (typeof tag !== 'string') { console.error('Tag must be a valid string'); From 5028f4adf41b95efcede07a9dcc1c8939ec703c8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 23:19:10 +0700 Subject: [PATCH 078/109] Better shouldComponentUpdate detection in withFragments --- client/coral-framework/hocs/withFragments.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index e6aca318a..9bc06290e 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -86,9 +86,9 @@ export default (fragments) => hoistStatics((BaseComponent) => { } shouldComponentUpdate(next) { + const onlyQueryDataChanges = this.shallowChanges.every((key) => this.fragmentKeys.indexOf(key) >= 0); - // If only query data was changed. - if (this.queryDataHasChanged && this.shallowChanges.every((key) => this.fragmentKeys.indexOf(key) >= 0)) { + if (onlyQueryDataChanges) { return this.queryDataHasChanged; } From 58a5d5eea835297b2cece69a9e82973e0e854521 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 23:20:05 +0700 Subject: [PATCH 079/109] Refactor Moderation Comment a little bit --- .../routes/Moderation/components/Comment.js | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 60077e80a..8d543e97b 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -20,6 +20,31 @@ import t, {timeago} from 'coral-framework/services/i18n'; class Comment extends React.Component { + showSuspenUserDialog = () => { + const {comment, showSuspendUserDialog} = this.props; + return showSuspendUserDialog({ + userId: comment.user.id, + username: comment.user.username, + commentId: comment.id, + commentStatus: comment.status, + }); + }; + + showBanUserDialog = () => { + const {comment, showBanUserDialog} = this.props; + return showBanUserDialog({ + userId: comment.user.id, + username: comment.user.username, + commentId: comment.id, + commentStatus: comment.status, + }); + }; + + viewUserDetail = () => { + const {viewUserDetail, comment} = this.props; + return viewUserDetail(comment.user.id); + }; + render() { const { actions = [], @@ -43,20 +68,6 @@ class Comment extends React.Component { let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; - const showSuspenUserDialog = () => this.props.showSuspendUserDialog({ - userId: comment.user.id, - username: comment.user.username, - commentId: comment.id, - commentStatus: comment.status, - }); - - const showBanUserDialog = () => this.props.showBanUserDialog({ - userId: comment.user.id, - username: comment.user.username, - commentId: comment.id, - commentStatus: comment.status, - }); - const queryData = {root, comment, asset: comment.asset}; return ( @@ -69,7 +80,7 @@ class Comment extends React.Component {

    { ( - viewUserDetail(comment.user.id)}> + {comment.user.username} ) @@ -86,11 +97,11 @@ class Comment extends React.Component { + onClick={this.showSuspenUserDialog}> Suspend User + onClick={this.showBanUserDialog}> Ban User From cf1c93f395411d71eb56c9d1ecb1e3e031a81864 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 18 Aug 2017 23:37:53 +0700 Subject: [PATCH 080/109] Add comments --- client/coral-embed-stream/src/containers/Comment.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 5f3ee0c9d..005a24ae1 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -18,6 +18,12 @@ const slots = [ 'commentAvatar' ]; +/** + * withAnimateEnter is a HOC that passes a property `animateEnter` to the + * underlying BaseComponent. It must be a direct child of a `TransitionGroup` + * from https://github.com/reactjs/react-transition-group and as such must + * be the uppermost HOC applied to the BaseComponent. + */ const withAnimateEnter = hoistStatics((BaseComponent) => { class WithAnimateEnter extends React.Component { state = { From f49591225f314b3e0b003b6e7e6680a5b624362e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Sat, 19 Aug 2017 00:10:31 +0700 Subject: [PATCH 081/109] Use UserDetailComment container --- client/coral-admin/src/components/UserDetail.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 9ee16935f..11cd444e1 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Comment from './UserDetailComment'; +import Comment from '../containers/UserDetailComment'; import styles from './UserDetail.css'; import {Icon, Button, Drawer, Spinner} from 'coral-ui'; import {Slot} from 'coral-framework/components'; @@ -99,7 +99,7 @@ export default class UserDetail extends React.Component { {new Date(user.created_at).toLocaleString()} - {user.profiles.map(({id}) => + {user.profiles.map(({id}) =>
  • Email: From 1b09825602594913e88c7012a34b1a0535297f36 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 Aug 2017 11:50:51 -0600 Subject: [PATCH 082/109] added more debugging to redis, improved retry --- config.js | 18 +++++++++++ docs/_docs/02-01-configuration.md | 15 +++++++++ graph/context.js | 2 +- graph/subscriptions.js | 4 +-- middleware/pubsub.js | 3 +- services/pubsub.js | 29 +++++++++-------- services/redis.js | 54 +++++++++++++++++++++++++------ 7 files changed, 96 insertions(+), 29 deletions(-) diff --git a/config.js b/config.js index a92802ee7..22720a36a 100644 --- a/config.js +++ b/config.js @@ -8,6 +8,7 @@ require('env-rewrite').rewrite(); const uniq = require('lodash/uniq'); +const ms = require('ms'); //============================================================================== // CONFIG INITIALIZATION @@ -84,6 +85,23 @@ const CONFIG = { MONGO_URL: process.env.TALK_MONGO_URL, REDIS_URL: process.env.TALK_REDIS_URL, + // REDIS_RECONNECTION_MAX_ATTEMPTS is the amount of attempts that a redis + // connection will attempt to reconnect before aborting with an error. + REDIS_RECONNECTION_MAX_ATTEMPTS: parseInt(process.env.TALK_REDIS_RECONNECTION_MAX_ATTEMPTS || '100'), + + // REDIS_RECONNECTION_MAX_RETRY_TIME is the time in string format for the + // maximum amount of time that a client can be considered "connecting" before + // attempts at reconnection are aborted with an error. + REDIS_RECONNECTION_MAX_RETRY_TIME: ms(process.env.TALK_REDIS_RECONNECTION_MAX_RETRY_TIME || '1 min'), + + // REDIS_RECONNECTION_BACKOFF_FACTOR is the factor that will be multiplied + // against the current attempt count inbetween attempts to connect to redis. + REDIS_RECONNECTION_BACKOFF_FACTOR: ms(process.env.TALK_REDIS_RECONNECTION_BACKOFF_FACTOR || '500 ms'), + + // REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME is the minimum time used to delay + // before attempting to reconnect to redis. + REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME: ms(process.env.TALK_REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME || '1 sec'), + //------------------------------------------------------------------------------ // Server Config //------------------------------------------------------------------------------ diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index 8f76ad999..58c828f9a 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -52,6 +52,21 @@ These are only used during the webpack build. - `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database. - `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database. +#### Advanced + +- `TALK_REDIS_RECONNECTION_MAX_ATTEMPTS` (_optional_) - the amount of attempts + that a redis connection will attempt to reconnect before aborting with an + error. (Default `100`) +- `TALK_REDIS_RECONNECTION_MAX_RETRY_TIME` (_optional_) - the time in string + format for the maximum amount of time that a client can be considered + "connecting" before attempts at reconnection are aborted with an error. + (Default `1 min`) +- `TALK_REDIS_RECONNECTION_BACKOFF_FACTOR` (_optional_) - the time factor that + will be multiplied against the current attempt count inbetween attempts to + connect to redis. (Default `500 ms`) +- `TALK_REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME` (_optional_) - the minimum time + used to delay before attempting to reconnect to redis. (Default `1 sec`) + ### Server - `TALK_ROOT_URL` (*required*) - root url of the installed application externally diff --git a/graph/context.js b/graph/context.js index cffec0896..8d7c740b6 100644 --- a/graph/context.js +++ b/graph/context.js @@ -53,7 +53,7 @@ class Context { this.plugins = decorateContextPlugins(this, contextPlugins); // Bind the publish/subscribe to the context. - this.pubsub = pubsub.createClient(); + this.pubsub = pubsub.getClient(); } } diff --git a/graph/subscriptions.js b/graph/subscriptions.js index adac849e0..f89b41d3a 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -127,15 +127,13 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu }), }); -const pubsubClient = pubsub.createClientFactory(); - /** * This creates a new subscription manager. */ const createSubscriptionManager = (server) => new SubscriptionServer({ subscriptionManager: new SubscriptionManager({ schema, - pubsub: pubsubClient(), + pubsub: pubsub.getClient(), setupFunctions, }), onConnect: ({token}, connection) => { diff --git a/middleware/pubsub.js b/middleware/pubsub.js index 15f7c51a2..957a523a7 100644 --- a/middleware/pubsub.js +++ b/middleware/pubsub.js @@ -1,12 +1,11 @@ const pubsub = require('../services/pubsub'); -const pubsubClient = pubsub.createClientFactory(); // To handle dependancy injection safer, we inject the pubsub handle onto the // request object. module.exports = (req, res, next) => { // Attach the pubsub handle to the requests. - req.pubsub = pubsubClient(); + req.pubsub = pubsub.getClient(); // Forward on the request. next(); diff --git a/services/pubsub.js b/services/pubsub.js index 780aeba16..9fc2ae902 100644 --- a/services/pubsub.js +++ b/services/pubsub.js @@ -1,23 +1,24 @@ const {RedisPubSub} = require('graphql-redis-subscriptions'); +const {connectionOptions, attachMonitors} = require('./redis'); -const {connectionOptions} = require('./redis'); +/** + * getClient returns the pubsub singleton for this instance. + */ +let pubsub = null; +const getClient = () => { + if (pubsub !== null) { + return pubsub; + } -const createClient = () => new RedisPubSub({connection: connectionOptions}); + pubsub = new RedisPubSub({connection: connectionOptions}); -const createClientFactory = () => { - let ins = null; - return () => { - if (ins) { - return ins; - } + // Attach the node monitors to the subscriber + publishers. + attachMonitors(pubsub.redisPublisher); + attachMonitors(pubsub.redisSubscriber); - ins = createClient(); - - return ins; - }; + return pubsub; }; module.exports = { - createClient, - createClientFactory + getClient, }; diff --git a/services/redis.js b/services/redis.js index 1983249f1..b87e5e9fd 100644 --- a/services/redis.js +++ b/services/redis.js @@ -1,45 +1,80 @@ const redis = require('redis'); const debug = require('debug')('talk:services:redis'); +const enabled = require('debug').enabled('talk:services:redis'); const { - REDIS_URL + REDIS_URL, + REDIS_RECONNECTION_MAX_ATTEMPTS, + REDIS_RECONNECTION_MAX_RETRY_TIME, + REDIS_RECONNECTION_BACKOFF_FACTOR, + REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME, } = require('../config'); +const attachMonitors = (client) => { + debug('client created'); + + // Debug events. + if (enabled) { + client.on('ready', () => debug('client ready')); + client.on('connect', () => debug('client connected')); + client.on('reconnecting', () => debug('client connection lost, attempting to reconnect')); + client.on('end', () => debug('client ended')); + } + + // Error events. + client.on('error', (err) => { + if (err) { + console.error('Error connecting to redis:', err); + } + }); +}; + const connectionOptions = { url: REDIS_URL, retry_strategy: function(options) { - if (options.error && options.error.code === 'ECONNREFUSED') { + if (options.error && options.error.code !== 'ECONNREFUSED') { + + debug('retry strategy: none, an error occured'); // End reconnecting on a specific error and flush all commands with a individual error - return new Error('The server refused the connection'); + return options.error; } - if (options.total_retry_time > 1000 * 60 * 60) { + if (options.total_retry_time > REDIS_RECONNECTION_MAX_RETRY_TIME) { + + debug('retry strategy: none, exhausted retry time'); // End reconnecting after a specific timeout and flush all commands with a individual error return new Error('Retry time exhausted'); } - if (options.times_connected > 10) { + if (options.attempt > REDIS_RECONNECTION_MAX_ATTEMPTS) { + + debug('retry strategy: none, exhausted retry attempts'); // End reconnecting with built in error return undefined; } // reconnect after - return Math.max(options.attempt * 100, 3000); + const delay = Math.max(options.attempt * REDIS_RECONNECTION_BACKOFF_FACTOR, REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME); + + debug(`retry strategy: try to reconnect ${delay} ms from now`); + + return delay; } }; const createClient = () => { let client = redis.createClient(connectionOptions); + // Attach the monitors that will print debug messages to the console. + attachMonitors(client); + client.ping((err) => { if (err) { console.error('Can\'t ping the redis server!'); throw err; } - - debug('connection established'); }); return client; @@ -47,12 +82,13 @@ const createClient = () => { module.exports = { connectionOptions, + attachMonitors, createClient, createClientFactory: () => { let client = null; return () => { - if (client) { + if (client !== null) { return client; } From b738961e1cd4f905a04850baaabf589b2bbe506f Mon Sep 17 00:00:00 2001 From: Clint Brown Date: Sun, 20 Aug 2017 23:29:03 +1000 Subject: [PATCH 083/109] Fix paths when using TALK_ROOT_URL_MOUNT_PATH --- app.js | 14 +------------- .../talk-plugin-moderation/ModerationLink.js | 3 ++- graph/subscriptions.js | 4 +++- url.js | 19 +++++++++++++++++++ views/article.ejs | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 url.js diff --git a/app.js b/app.js index 9abb921ae..741dac93f 100644 --- a/app.js +++ b/app.js @@ -5,10 +5,9 @@ const path = require('path'); const helmet = require('helmet'); const compression = require('compression'); const cookieParser = require('cookie-parser'); -const {ROOT_URL, ROOT_URL_MOUNT_PATH} = require('./config'); +const {BASE_URL, BASE_PATH, MOUNT_PATH} = require('./url'); const routes = require('./routes'); const debug = require('debug')('talk:app'); -const {URL} = require('url'); const app = express(); @@ -51,17 +50,6 @@ app.set('view engine', 'ejs'); // ROUTES //============================================================================== -// Set the BASE_URL as the ROOT_URL, here we derive the root url by ensuring -// that it ends in a `/`. -const BASE_URL = ROOT_URL && ROOT_URL.length > 0 && ROOT_URL[ROOT_URL.length - 1] === '/' ? ROOT_URL : `${ROOT_URL}/`; - -// The BASE_PATH is simply the path component of the BASE_URL. -const BASE_PATH = new URL(BASE_URL).pathname; - -// The MOUNT_PATH is derived from the BASE_PATH, if it is provided and enabled. -// This will mount all the application routes onto it. -const MOUNT_PATH = ROOT_URL_MOUNT_PATH ? BASE_PATH : '/'; - // Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will // make them available on the templates and the routers. app.locals.BASE_URL = BASE_URL; diff --git a/client/talk-plugin-moderation/ModerationLink.js b/client/talk-plugin-moderation/ModerationLink.js index 020b8c345..1a10160ee 100644 --- a/client/talk-plugin-moderation/ModerationLink.js +++ b/client/talk-plugin-moderation/ModerationLink.js @@ -2,10 +2,11 @@ import React, {PropTypes} from 'react'; import styles from './styles.css'; import t from 'coral-framework/services/i18n'; +import {BASE_PATH} from 'coral-framework/constants/url'; const ModerationLink = (props) => props.isAdmin ? ( diff --git a/graph/subscriptions.js b/graph/subscriptions.js index adac849e0..91ab3c091 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -26,6 +26,8 @@ const { SUBSCRIBE_ALL_USERNAME_REJECTED, } = require('../perms/constants'); +const {BASE_PATH} = require('../url'); + /** * Plugin support requires that we merge in existing setupFunctions with our new * plugin based ones. This allows plugins to extend existing setupFunctions as well @@ -172,7 +174,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({ keepAlive: ms(KEEP_ALIVE) }, { server, - path: '/api/v1/live' + path: '${BASE_PATH}api/v1/live' }); module.exports = { diff --git a/url.js b/url.js new file mode 100644 index 000000000..b39a9ab35 --- /dev/null +++ b/url.js @@ -0,0 +1,19 @@ +const {ROOT_URL, ROOT_URL_MOUNT_PATH} = require('./config'); +const {URL} = require('url'); + +// Set the BASE_URL as the ROOT_URL, here we derive the root url by ensuring +// that it ends in a `/`. +const BASE_URL = ROOT_URL && ROOT_URL.length > 0 && ROOT_URL[ROOT_URL.length - 1] === '/' ? ROOT_URL : `${ROOT_URL}/`; + +// The BASE_PATH is simply the path component of the BASE_URL. +const BASE_PATH = new URL(BASE_URL).pathname; + +// The MOUNT_PATH is derived from the BASE_PATH, if it is provided and enabled. +// This will mount all the application routes onto it. +const MOUNT_PATH = ROOT_URL_MOUNT_PATH ? BASE_PATH : '/'; + +module.exports = { + BASE_URL: BASE_URL, + BASE_PATH: BASE_PATH, + MOUNT_PATH: MOUNT_PATH, +} diff --git a/views/article.ejs b/views/article.ejs index f0a3dad98..0f8c75594 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -22,7 +22,7 @@

    <%= title %>

    <%= body %>

    -

    Admin - All Assets

    +

    Admin - All Assets