From 63347dc0ebd59f77df3077ad9366359470e7f88a Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 25 Jan 2018 14:50:06 -0500 Subject: [PATCH 01/16] Change s to z for zen mode shortcut and update copy and docs --- .../src/components/ModerationKeysModal.js | 2 +- .../Moderation/components/Moderation.js | 4 ++-- .../03-03-product-guide-moderator-features.md | 22 ++++++++++--------- locales/en.yml | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/client/coral-admin/src/components/ModerationKeysModal.js b/client/coral-admin/src/components/ModerationKeysModal.js index c8066b81d..4e0eb3ac3 100644 --- a/client/coral-admin/src/components/ModerationKeysModal.js +++ b/client/coral-admin/src/components/ModerationKeysModal.js @@ -23,7 +23,7 @@ export default class ModerationKeysModal extends React.Component { 'ctrl+f': 'modqueue.toggle_search', t: 'modqueue.next_queue', [`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue', - s: 'modqueue.singleview', + z: 'modqueue.singleview', '?': 'modqueue.thismenu', }, }, diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index 760dfcbb1..c1de008fe 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -19,7 +19,7 @@ class Moderation extends Component { componentWillMount() { const { toggleModal, singleView } = this.props; - key('s', () => singleView()); + key('z', () => singleView()); key('shift+/', () => toggleModal(true)); key('esc', () => toggleModal(false)); key('ctrl+f', () => this.openSearch()); @@ -113,7 +113,7 @@ class Moderation extends Component { }; componentWillUnmount() { - key.unbind('s'); + key.unbind('z'); key.unbind('shift+/'); key.unbind('esc'); key.unbind('ctrl+f'); diff --git a/docs/_docs/03-03-product-guide-moderator-features.md b/docs/_docs/03-03-product-guide-moderator-features.md index 064257623..445f1a308 100644 --- a/docs/_docs/03-03-product-guide-moderator-features.md +++ b/docs/_docs/03-03-product-guide-moderator-features.md @@ -133,16 +133,18 @@ Talk also allows you to moderate a commenters recent comments from this view. Talk also supports a number of keyboard shortcuts that moderators can leverage to moderate quickly: -| Shortcut | Action | -| -------- | ------------------------------- | -| `j` | Go to the next comment | -| `k` | Go to the previous comment | -| `ctrl+f` | Open search | -| `t` | Switch queues | -| `s` | Toggle single comment edit view | -| `?` | Open this menu | -| `d` | Approve | -| `f` | Reject | +| Shortcut | Action | +| -------- | -------------------------- | +| `j` | Go to the next comment | +| `k` | Go to the previous comment | +| `ctrl+f` | Open search | +| `t` | Switch queues | +| `z` | Zen mode | +| `?` | Open this menu | +| `d` | Approve | +| `f` | Reject | + +Note: "Zen mode" allows a moderator to view and action only one comment at a time. Namaste! ### Stories diff --git a/locales/en.yml b/locales/en.yml index 8a9ce4bcb..efcd7a1d9 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -330,7 +330,7 @@ en: shortcuts: "Shortcuts" sort: "Sort" show_shortcuts: "Show Shortcuts" - singleview: "Toggle single comment edit view" + singleview: "Zen mode" thismenu: "Open this menu" jump_to_queue: "Jump to specific queue" thousand: k From fe4cddda60a6572d390b82a01c076d36abca7e4c Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 25 Jan 2018 14:51:15 -0500 Subject: [PATCH 02/16] Copy change --- docs/_docs/03-03-product-guide-moderator-features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/03-03-product-guide-moderator-features.md b/docs/_docs/03-03-product-guide-moderator-features.md index 445f1a308..671461f31 100644 --- a/docs/_docs/03-03-product-guide-moderator-features.md +++ b/docs/_docs/03-03-product-guide-moderator-features.md @@ -144,7 +144,7 @@ to moderate quickly: | `d` | Approve | | `f` | Reject | -Note: "Zen mode" allows a moderator to view and action only one comment at a time. Namaste! +Note: "Zen mode" allows a moderator to view and action only one comment at a time. Enjoy the silence! ### Stories From 0507e750b98c06c8809b973f74f58fb074e42338 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 25 Jan 2018 15:00:34 -0500 Subject: [PATCH 03/16] Update translations --- locales/da.yml | 2 +- locales/es.yml | 2 +- locales/fr.yml | 2 +- locales/nl_NL.yml | 2 +- locales/pt_BR.yml | 2 +- locales/zh_CN.yml | 2 +- locales/zh_TW.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/locales/da.yml b/locales/da.yml index d4544043c..b3cf32ea4 100644 --- a/locales/da.yml +++ b/locales/da.yml @@ -274,7 +274,7 @@ da: shift_key: "⇧" shortcuts: "Genveje" show_shortcuts: "Vis genveje" - singleview: "Skift enkeltkommentar redigerings visning" + singleview: "Zen mode" thismenu: "Åben denne menu" thousand: "k" try_these: "Prøv disse" diff --git a/locales/es.yml b/locales/es.yml index 010119728..6b695851e 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -291,7 +291,7 @@ es: shortcuts: Atajos sort: "Ordenar" show_shortcuts: "Mostrar Atajos" - singleview: "Colocar vista de edición de comentario único" + singleview: "Modo zen" thismenu: "Abrir este menu" thousand: k try_these: "Intentar estos" diff --git a/locales/fr.yml b/locales/fr.yml index e9f291b65..890582f7a 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -225,7 +225,7 @@ fr: shift_key: ⇧ shortcuts: Raccourcis show_shortcuts: "Afficher les raccourcis" - singleview: "Passer en mode d'édition de commentaire unique" + singleview: "Mode zen" spam_ads: Spam / Publicités thismenu: "Ouvrir ce menu" thousand: k diff --git a/locales/nl_NL.yml b/locales/nl_NL.yml index 43e7d90f5..be8888600 100644 --- a/locales/nl_NL.yml +++ b/locales/nl_NL.yml @@ -325,7 +325,7 @@ nl_NL: shortcuts: "Sneltoetsen" sort: "Sorteer" show_shortcuts: "Toon sneltoetsen" - singleview: "Schakel wijzigen enkele reactie aan of uit" + singleview: "Zen-modus" thismenu: "Open dit menu" jump_to_queue: "Spring naar specifieke wachtrij" thousand: k diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index 7ed111287..bc36409cc 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -276,7 +276,7 @@ pt_BR: shift_key: "⇧" shortcuts: "Atalhos" show_shortcuts: "Ver atalhos" - singleview: "Alternar vista de edição de comentário único" + singleview: "Modo zen" spam_ads: Spam/Anuncios thismenu: "Abra este menu" thousand: k diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml index 480115cbc..1b1709a38 100644 --- a/locales/zh_CN.yml +++ b/locales/zh_CN.yml @@ -290,7 +290,7 @@ zh_CN: shortcuts: "快捷键" sort: "排序" show_shortcuts: "显示快捷键" - singleview: "展开单评论编辑视图" + singleview: "禅宗模式" thismenu: "开启该菜单" jump_to_queue: "跳转到特定序列" thousand: "千" diff --git a/locales/zh_TW.yml b/locales/zh_TW.yml index f18971a8f..0edceedb4 100644 --- a/locales/zh_TW.yml +++ b/locales/zh_TW.yml @@ -290,7 +290,7 @@ zh_TW: shortcuts: "快捷鍵" sort: "排序" show_shortcuts: "顯示快捷鍵" - singleview: "切換單個評論編輯視圖" + singleview: "禪宗模式" thismenu: "打開這個菜單" jump_to_queue: "跳轉到特定隊列" thousand: 千 From 4be9e74ed9309028af4845edd74ef57fb4794b11 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 26 Jan 2018 12:58:16 -0700 Subject: [PATCH 04/16] removed unused code --- bin/verifications/database/action_counts.js | 194 ------------------ bin/verifications/database/comment_replies.js | 140 ------------- bin/verifications/database/index.js | 10 - 3 files changed, 344 deletions(-) delete mode 100644 bin/verifications/database/action_counts.js delete mode 100644 bin/verifications/database/comment_replies.js delete mode 100644 bin/verifications/database/index.js diff --git a/bin/verifications/database/action_counts.js b/bin/verifications/database/action_counts.js deleted file mode 100644 index 684c17bca..000000000 --- a/bin/verifications/database/action_counts.js +++ /dev/null @@ -1,194 +0,0 @@ -const UserModel = require('../../../models/user'); -const CommentModel = require('../../../models/comment'); -const ActionsService = require('../../../services/actions'); -const { arrayJoinBy } = require('../../../graph/loaders/util'); -const { get } = require('lodash'); -const debug = require('debug')('talk:cli:verify'); - -const MODELS = [UserModel, CommentModel]; - -async function processBatch(Model, documents) { - // Get an array of all the document id's. - const documentIDs = documents.map(({ id }) => id); - - // Store all the operations on this batch in this array that we'll return - // later. - const operations = []; - - // Get the action summaries for this batch. - const totalActionSummaries = await ActionsService.getActionSummaries( - documentIDs - ).then(arrayJoinBy(documentIDs, 'item_id')); - - // Iterate over the documents. - for (let i = 0; i < documents.length; i++) { - const document = documents[i]; - const actionSummaries = totalActionSummaries[i]; - - let ops = []; - - for (const actionSummary of actionSummaries) { - if (actionSummary.group_id === null) { - continue; - } - - // And we generate the group id. - const ACTION_TYPE = actionSummary.action_type.toLowerCase(); - const GROUP_ID = actionSummary.group_id.toLowerCase(); - - if (GROUP_ID.length <= 0) { - continue; - } - - // And we add a new batch operation if the action summary is associated - // with a group. - const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`; - - // Check that the action summaries match the cached counts. - if ( - get(document, ['action_counts', ACTION_COUNT_FIELD]) !== - actionSummary.count - ) { - // Batch updates for those changes. - ops.push({ - [`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count, - }); - } - } - - // Group all the action summaries together from all the different group - // ids. - const groupedActionSummaries = actionSummaries.reduce( - (acc, actionSummary) => { - // action_type is already snake cased (as it would have had to be when it - // was inserted in the database). - const ACTION_TYPE = actionSummary.action_type.toLowerCase(); - - if (!(ACTION_TYPE in acc)) { - acc[ACTION_TYPE] = 0; - } - - acc[ACTION_TYPE] += actionSummary.count; - - return acc; - }, - {} - ); - - for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) { - const count = groupedActionSummaries[ACTION_COUNT_FIELD]; - - // Check that the action summaries match the cached counts. - if (get(document, ['action_counts', ACTION_COUNT_FIELD]) !== count) { - // Batch updates for those changes. - ops.push({ - [`action_counts.${ACTION_COUNT_FIELD}`]: count, - }); - } - } - - // If this comment has action summaries that should be updated, then - // perform an update! - if (ops.length > 0) { - operations.push({ - updateOne: { - filter: { - id: document.id, - }, - update: { - $set: Object.assign({}, ...ops), - }, - }, - }); - } - } - - return operations; -} - -module.exports = async ({ fix, batch }) => { - for (const Model of MODELS) { - const cursor = Model.collection - .find({}) - .project({ - id: 1, - action_counts: 1, - }) - .sort({ created_at: 1 }); - - let operations = []; - let documents = []; - - // While there are documents to process. - while (await cursor.hasNext()) { - // Load the document. - const document = await cursor.next(); - - // Push the document into the documents array. - documents.push(document); - - // Check to see if the length of the documents array requires us to - // process it. - if (documents.length > batch) { - // Process this batch. - let batchOperations = await processBatch(Model, documents); - - // Push the batch operations into the model operations. - operations.push(...batchOperations); - - // Clear this batch contents. - documents = []; - } - } - - // Check to see if there are any documents left over. - if (documents.length > 0) { - // Process this batch. - let batchOperations = await processBatch(Model, documents); - - // Push the batch operations into the model operations. - operations.push(...batchOperations); - } - - const OPERATIONS_LENGTH = operations.length; - - console.log( - `action_counts.js: ${OPERATIONS_LENGTH} ${ - Model.collection.name - } need their action counts fixed.` - ); - - // If fix was enabled, execute the batch writes. - if (OPERATIONS_LENGTH > 0) { - if (fix) { - debug( - `action_counts.js: fixing ${OPERATIONS_LENGTH} ${ - Model.collection.name - }...` - ); - - while (operations.length) { - let result = await Model.collection.bulkWrite( - operations.splice(0, batch) - ); - - debug( - `action_counts.js: fixed batch of ${result.modifiedCount} ${ - Model.collection.name - }.` - ); - } - - console.log( - `action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${ - Model.collection.name - }.` - ); - } else { - console.warn( - 'Skipping fixing, --fix was not enabled, pass --fix to fix these errors' - ); - } - } - } -}; diff --git a/bin/verifications/database/comment_replies.js b/bin/verifications/database/comment_replies.js deleted file mode 100644 index 309023c75..000000000 --- a/bin/verifications/database/comment_replies.js +++ /dev/null @@ -1,140 +0,0 @@ -const CommentModel = require('../../../models/comment'); -const { singleJoinBy } = require('../../../graph/loaders/util'); -const debug = require('debug')('talk:cli:verify'); - -const getBatch = async (limit, offset) => - CommentModel.find({}) - .select({ id: 1, action_counts: 1, reply_count: 1 }) - .limit(limit) - .skip(offset) - .sort('created_at'); - -module.exports = async ({ fix, limit, batch }) => { - let operations = []; - - // Count how many comments there are to process. - const totalCount = await CommentModel.count(); - - let offset = 0; - let comments = []; - let commentIDs = []; - - console.log(`Processing ${totalCount} comments in batches of ${limit}...`); - - // Keep processing documents until there are is none left. - while (offset < totalCount) { - // Get a batch of comments. - comments = await getBatch(batch, offset); - commentIDs = comments.map(({ id }) => id); - - // Get their reply counts. - let allReplyCounts = await CommentModel.aggregate([ - { - $match: { - parent_id: { - $in: commentIDs, - }, - status: { - $in: ['NONE', 'ACCEPTED'], - }, - }, - }, - { - $group: { - _id: '$parent_id', - count: { - $sum: 1, - }, - }, - }, - ]) - .then(singleJoinBy(commentIDs, '_id')) - .then(results => results.map(result => (result ? result.count : 0))); - - // Loop over the comments, with their action summaries. - for (let i = 0; i < comments.length; i++) { - let comment = comments[i]; - let replyCount = allReplyCounts[i]; - - // And check to see if the action summaries we just computed match what is - // currently set for the comments. - let commentOperations = []; - - // If the reply count needs to be updated, then update it! - if (comment.reply_count !== replyCount) { - commentOperations.push({ - reply_count: replyCount, - }); - } - - // If this comment has action summaries that should be updated, then - // perform an update! - if (commentOperations.length > 0) { - operations.push({ - updateOne: { - filter: { - id: comment.id, - }, - update: { - $set: Object.assign({}, ...commentOperations), - }, - }, - }); - } - } - - debug(`Processed batch of ${comments.length} comments.`); - - if (operations.length >= limit) { - debug( - `Queued operations are ${ - operations.length - }, reached limit of ${limit}, not processing any more.` - ); - - if (operations.length > limit) { - debug( - `${operations.length - - limit} operations have been truncated to enforce the limit` - ); - } - - break; - } - - offset += batch; - } - - const OPERATIONS_LENGTH = operations.length; - - if (limit < Infinity && offset + comments.length < totalCount) { - console.log( - `Processed ${offset + - comments.length}/${totalCount} comments because we reached the update limit of ${limit}.` - ); - } else { - console.log(`Processed all ${totalCount} comments.`); - } - - console.log(`${OPERATIONS_LENGTH} documents need fixing.`); - - // If fix was enabled, execute the batch writes. - if (OPERATIONS_LENGTH > 0) { - if (fix) { - debug(`Fixing ${OPERATIONS_LENGTH} documents...`); - - while (operations.length) { - let batchOperations = operations.splice(0, batch); - let result = await CommentModel.collection.bulkWrite(batchOperations); - - debug(`Fixed batch of ${result.modifiedCount} documents.`); - } - - console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`); - } else { - console.warn( - 'Skipping fixing, --fix was not enabled, pass --fix to fix these errors' - ); - } - } -}; diff --git a/bin/verifications/database/index.js b/bin/verifications/database/index.js deleted file mode 100644 index 58a46bd85..000000000 --- a/bin/verifications/database/index.js +++ /dev/null @@ -1,10 +0,0 @@ -// This will import all the verifications that should be run by the: -// -// cli verify database -// -// command. They exist in the form: -// -// async ({fix = false, batch = 1000}) => {} -// -// where their options are derived. -module.exports = [require('./comment_replies'), require('./action_counts')]; From 78019396e2d53ec178ac39d68f0cbcc1bcb89638 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 29 Jan 2018 13:47:46 +0100 Subject: [PATCH 05/16] Use same created_at --- services/comments.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/services/comments.js b/services/comments.js index e18e04970..fd986ae3f 100644 --- a/services/comments.js +++ b/services/comments.js @@ -19,6 +19,7 @@ module.exports = class CommentsService { static async publicCreate(input) { // Extract the parent_id from the comment, if there is one. const { status = 'NONE', parent_id = null } = input; + const created_at = new Date(); // Check to see if we are replying to a comment, and if that comment is // visible. @@ -37,14 +38,14 @@ module.exports = class CommentsService { ? [ { type: status, - created_at: new Date(), + created_at, }, ] : [], body_history: [ { body: input.body, - created_at: new Date(), + created_at, }, ], }, @@ -85,6 +86,7 @@ module.exports = class CommentsService { */ static async edit({ id, author_id, body, status }) { const EDITABLE_STATUSES = ['NONE', 'PREMOD', 'ACCEPTED']; + const created_at = new Date(); const query = { id, @@ -112,11 +114,11 @@ module.exports = class CommentsService { $push: { body_history: { body, - created_at: new Date(), + created_at, }, status_history: { type: status, - created_at: new Date(), + created_at, }, }, }); @@ -160,11 +162,11 @@ module.exports = class CommentsService { editedComment.body = body; editedComment.body_history.push({ body, - created_at: new Date(), + created_at, }); editedComment.status_history.push({ type: status, - created_at: new Date(), + created_at, }); // We should adjust the comment's status such that if it was approved @@ -192,7 +194,7 @@ module.exports = class CommentsService { $push: { status_history: { type: lastUnmoderatedStatus, - created_at: new Date(), + created_at, }, }, } @@ -202,7 +204,7 @@ module.exports = class CommentsService { editedComment.status = lastUnmoderatedStatus; editedComment.status_history.push({ type: lastUnmoderatedStatus, - created_at: new Date(), + created_at, }); } } From c12fe526b824778a7aa1e23b666c38b48b4c7f17 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 29 Jan 2018 14:52:09 +0100 Subject: [PATCH 06/16] Better detect edits --- .../src/routes/Moderation/graphql.js | 20 +++++++++++++++++-- graph/typeDefs.graphql | 8 ++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index c542b8f84..aa6267880 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -112,6 +112,10 @@ function getOlderDate(a, b) { function determineLatestChange(comment) { let lc = null; + comment.body_history.forEach(item => { + lc = getOlderDate(lc, item.created_at); + }); + comment.status_history.forEach(item => { lc = getOlderDate(lc, item.created_at); }); @@ -124,12 +128,16 @@ function determineLatestChange(comment) { } function reconstructPreviousCommentState(comment) { - const history = comment.status_history; + const statusHistory = comment.status_history; + const bodyHistory = comment.body_history; const actions = comment.actions; const lastChangeDate = determineLatestChange(comment); const previousComment = { ...comment, - status_history: history.filter( + body_history: bodyHistory.filter( + item => new Date(item.created_at) < lastChangeDate + ), + status_history: statusHistory.filter( item => new Date(item.created_at) < lastChangeDate ), actions: actions.filter(item => new Date(item.created_at) < lastChangeDate), @@ -145,6 +153,9 @@ function reconstructPreviousCommentState(comment) { previousComment.status_history.length - 1 ].type; + previousComment.body = + previousComment.body_history[previousComment.body_history.length - 1].body; + return previousComment; } @@ -383,6 +394,11 @@ export function handleIndicatorChange(root, comment, queueConfig) { export const subscriptionFields = ` status + body + body_history { + body + created_at + } actions { __typename created_at diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 550856082..cec8e0015 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -453,6 +453,11 @@ type EditInfo { editableUntil: Date } +type CommentBodyHistory { + body: String! + created_at: Date! +} + type CommentStatusHistory { type: COMMENT_STATUS! created_at: Date! @@ -471,6 +476,9 @@ type Comment { # The actual comment data. body: String! + # The body history of the comment. + body_history: [CommentBodyHistory!]! + # the tags on the comment tags: [TagLink!] From 2bfa99fa1a9ad73da822b7f2cbd1644e3217927f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 29 Jan 2018 14:52:26 +0100 Subject: [PATCH 07/16] Remove unused proptype --- client/coral-admin/src/routes/Configure/components/Configure.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index 2b3e44f1e..f81bf7735 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -86,7 +86,6 @@ export default class Configure extends Component { } Configure.propTypes = { - notify: PropTypes.func.isRequired, savePending: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, data: PropTypes.object.isRequired, From 5f608e9bc624ee8591273a289222a1a456f0779e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 29 Jan 2018 17:40:38 +0100 Subject: [PATCH 08/16] Fix moderator comments not showing up --- client/coral-embed-stream/src/graphql/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index dcded6b45..279a1dcf6 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -198,7 +198,7 @@ export default { { mutationResult: { data: { createComment: { comment } } } } ) => { if ( - (prev.me.role !== 'ADMIN' && + (!['ADMIN', 'MODERATOR'].includes(prev.me.role) && prev.asset.settings.moderation === 'PRE') || comment.status === 'PREMOD' || comment.status === 'REJECTED' || From bc42539fc452edd869c95d8f130684da348c6db8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 10:02:38 -0700 Subject: [PATCH 09/16] cleanup --- bin/cli-users | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bin/cli-users b/bin/cli-users index 6e901f529..cb87445c5 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -28,7 +28,6 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const USER_ROLES = require('../models/enum/user_roles'); const mongoose = require('../services/mongoose'); -const databaseVerifications = require('./verifications/database'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); @@ -65,6 +64,8 @@ async function deleteUser(userID) { console.warn("Removing user's actions"); + // FIXME: fix the counts. + // Remove all the user's actions. await ActionModel.where({ user_id: user.id }) .setOptions({ multi: true }) @@ -77,13 +78,6 @@ async function deleteUser(userID) { .setOptions({ multi: true }) .remove(); - console.warn('Updating the database indexes'); - - // Update the counts that might have changed. - for (const verification of databaseVerifications) { - await verification({ fix: true, limit: Infinity, batch: 1000 }); - } - console.warn('Removing the user'); // Remove the user. From c13ea699d9caac73dc4e8716f21b915bcff3e6cd Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 10:07:42 -0700 Subject: [PATCH 10/16] turned off nsp on CI --- circle.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/circle.yml b/circle.yml index d45828540..bd5c2c520 100644 --- a/circle.yml +++ b/circle.yml @@ -37,7 +37,7 @@ dependencies: # Install node dependencies. - yarn --version - - yarn global add node-gyp nsp --force + - yarn global add node-gyp --force - yarn post: @@ -59,8 +59,6 @@ test: - yarn test # Run the end to end tests - yarn e2e:ci - # Check dependancies using nsp. - - nsp check deployment: release: From e20e55b5892324eff3b7f4a5d9919836667cae34 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 10:35:10 -0700 Subject: [PATCH 11/16] removed outdated logic --- services/comments.js | 40 -------------------------------- test/server/services/comments.js | 10 ++++---- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/services/comments.js b/services/comments.js index e18e04970..a71afd92b 100644 --- a/services/comments.js +++ b/services/comments.js @@ -167,46 +167,6 @@ module.exports = class CommentsService { created_at: new Date(), }); - // We should adjust the comment's status such that if it was approved - // previously, we should mark the comment as 'NONE' or 'PREMOD', which ever - // was most recent if the new comment is destined to be `NONE` or `PREMOD`. - if (originalComment.status === 'ACCEPTED' && status === 'NONE') { - const lastUnmoderatedStatus = CommentsService.lastUnmoderatedStatus( - originalComment - ); - - // If the last moderated status was found and the current comment doesn't - // match this already. - if (lastUnmoderatedStatus && status !== lastUnmoderatedStatus) { - // Update the comment model (if at this point, the status is still - // accepted) with the previously unmoderated status - await CommentModel.update( - { - id, - status, - }, - { - $set: { - status: lastUnmoderatedStatus, - }, - $push: { - status_history: { - type: lastUnmoderatedStatus, - created_at: new Date(), - }, - }, - } - ); - - // Update the returned comment. - editedComment.status = lastUnmoderatedStatus; - editedComment.status_history.push({ - type: lastUnmoderatedStatus, - created_at: new Date(), - }); - } - } - await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); return editedComment; diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 2d0b37163..e0fc6da67 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -227,12 +227,12 @@ describe('services.CommentsService', () => { id: originalComment.id, author_id: '123', body: 'This is a body!', - status: 'NONE', + status: 'PREMOD', }); expect(editedComment).to.have.property('status', 'PREMOD'); - expect(editedComment.status_history).to.have.length(4); - expect(editedComment.status_history[3]).to.have.property( + expect(editedComment.status_history).to.have.length(3); + expect(editedComment.status_history[2]).to.have.property( 'type', 'PREMOD' ); @@ -240,8 +240,8 @@ describe('services.CommentsService', () => { retrivedComment = await CommentsService.findById(originalComment.id); expect(retrivedComment).to.have.property('status', 'PREMOD'); - expect(retrivedComment.status_history).to.have.length(4); - expect(retrivedComment.status_history[3]).to.have.property( + expect(retrivedComment.status_history).to.have.length(3); + expect(retrivedComment.status_history[2]).to.have.property( 'type', 'PREMOD' ); From 8d656b51a0455f91c4e8532c633ee3f8a3949242 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 13:56:09 -0700 Subject: [PATCH 12/16] introduced count adjusters for deleting a user --- bin/cli-users | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/bin/cli-users b/bin/cli-users index cb87445c5..ea308ae44 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -8,6 +8,7 @@ const util = require('./util'); const program = require('commander'); const inquirer = require('inquirer'); const { graphql } = require('graphql'); +const helpers = require('../services/migration/helpers'); const { stripIndent } = require('common-tags'); const Table = require('cli-table'); @@ -32,6 +33,19 @@ const mongoose = require('../services/mongoose'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); +/** + * transforms a specific action to a removal action on the target model. + */ +const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({ + query: { id: item_id }, + update: { + $inc: { + [`action_counts.${action_type.toLowerCase()}`]: -1, + [`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1, + }, + }, +}); + /** * Deletes a user and cleans up their associated verifications. */ @@ -62,9 +76,26 @@ async function deleteUser(userID) { return util.shutdown(); } + const { transformSingleWithCursor } = helpers({ + queryBatchSize: 10000, + updateBatchSize: 10000, + }); + console.warn("Removing user's actions"); - // FIXME: fix the counts. + // Remove all actions against comments. + await transformSingleWithCursor( + ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }), + actionDecrTransformer, + CommentModel + ); + + // Remove all actions against users. + await transformSingleWithCursor( + ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }), + actionDecrTransformer, + UserModel + ); // Remove all the user's actions. await ActionModel.where({ user_id: user.id }) @@ -73,6 +104,29 @@ async function deleteUser(userID) { console.warn("Removing user's comments"); + // Removes all the user's reply counts on each of the comments that they + // have commented on. + await transformSingleWithCursor( + CommentModel.collection.aggregate([ + { $match: { author_id: user.id } }, + { + $group: { + _id: '$parent_id', + count: { $sum: 1 }, + }, + }, + ]), + ({ _id: parent_id, count }) => ({ + query: { id: parent_id }, + update: { + $inc: { + reply_count: -1 * count, + }, + }, + }), + CommentModel + ); + // Remove all the user's comments. await CommentModel.where({ author_id: user.id }) .setOptions({ multi: true }) From 985fc05a47f73521af3f89750c85e0d23f5996c9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 15:29:55 -0700 Subject: [PATCH 13/16] only users that have approved usernames may comment --- perms/reducers/mutation.js | 4 ++- test/server/graph/mutations/createComment.js | 38 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/perms/reducers/mutation.js b/perms/reducers/mutation.js index 3e73afbc9..73ee7ef28 100644 --- a/perms/reducers/mutation.js +++ b/perms/reducers/mutation.js @@ -15,7 +15,9 @@ module.exports = (user, perm) => { case types.EDIT_COMMENT: // Anyone can do these things if they aren't suspended, banned, or blocked // as they're editing their username. - return !['UNSET', 'REJECTED'].includes(user.status.username.status); + return !['UNSET', 'REJECTED', 'CHANGED'].includes( + user.status.username.status + ); case types.ADD_COMMENT_TAG: case types.REMOVE_COMMENT_TAG: diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 720297e12..bd89b31a6 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -270,6 +270,44 @@ describe('graph.mutations.createComment', () => { }); }); + describe('user with different username statuses', () => { + beforeEach(() => AssetModel.create({ id: '123' })); + + [ + { status: 'UNSET', error: true }, + { status: 'SET', error: false }, + { status: 'APPROVED', error: false }, + { status: 'REJECTED', error: true }, + { status: 'CHANGED', error: true }, + ].forEach(({ status, error }) => { + describe(`user.status.username.status=${status}`, () => { + it(`${error ? 'can not' : 'can'} create a comment`, async () => { + const context = new Context({ + user: new UserModel({ status: { username: { status } } }), + }); + + const { data, errors } = await graphql(schema, query, {}, context); + + if (errors) { + console.error(errors); + } + expect(errors).to.be.undefined; + + if (error) { + expect(data.createComment).to.have.property('errors').not.null; + expect(data.createComment).to.have.property('comment').null; + } else { + if (data.createComment.errors) { + console.error(data.createComment.errors); + } + expect(data.createComment).to.have.property('errors').null; + expect(data.createComment).to.have.property('comment').not.null; + } + }); + }); + }); + }); + describe('users with different roles', () => { beforeEach(() => AssetModel.create({ id: '123' })); From d25ca0ef6f0689c51cf979dc7d4073dfe3a80f5c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 15:36:59 -0700 Subject: [PATCH 14/16] more test criteria --- test/server/graph/mutations/createComment.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index bd89b31a6..058b19a41 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -296,6 +296,11 @@ describe('graph.mutations.createComment', () => { if (error) { expect(data.createComment).to.have.property('errors').not.null; expect(data.createComment).to.have.property('comment').null; + expect(data.createComment.errors).to.have.length(1); + expect(data.createComment.errors[0]).to.have.property( + 'translation_key', + 'NOT_AUTHORIZED' + ); } else { if (data.createComment.errors) { console.error(data.createComment.errors); From 335349963cb1c94f46395a51a56204fcb344a2d1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 29 Jan 2018 17:35:09 -0700 Subject: [PATCH 15/16] adjusted flow for changed usernames, e2e --- .../tabs/stream/components/ChangedUsername.js | 17 ++++++ .../src/tabs/stream/components/Stream.js | 4 ++ locales/en.yml | 2 + test/e2e/specs/04_userStatus.js | 54 ++++++++++++++++--- 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 client/coral-embed-stream/src/tabs/stream/components/ChangedUsername.js diff --git a/client/coral-embed-stream/src/tabs/stream/components/ChangedUsername.js b/client/coral-embed-stream/src/tabs/stream/components/ChangedUsername.js new file mode 100644 index 000000000..3a0ba5790 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/stream/components/ChangedUsername.js @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; +import t from 'coral-framework/services/i18n'; +import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox'; + +class ChangeUsername extends Component { + render() { + return ( + +
+ {t('framework.changed_name.msg')} +
+
+ ); + } +} + +export default ChangeUsername; diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index 6288a2b6b..65020cb08 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -25,6 +25,7 @@ import AllCommentsPane from './AllCommentsPane'; import ExtendableTabPanel from '../../../containers/ExtendableTabPanel'; import styles from './Stream.css'; +import ChangedUsername from './ChangedUsername'; class Stream extends React.Component { constructor(props) { @@ -237,6 +238,7 @@ class Stream extends React.Component { const banned = get(user, 'status.banned.status'); const suspensionUntil = get(user, 'status.suspension.until'); const rejectedUsername = get(user, 'status.username.status') === 'REJECTED'; + const changedUsername = get(user, 'status.username.status') === 'CHANGED'; const temporarilySuspended = user && suspensionUntil && new Date(suspensionUntil) > new Date(); @@ -246,6 +248,7 @@ class Stream extends React.Component { ((!banned && !temporarilySuspended && !rejectedUsername && + !changedUsername && !highlightedComment) || keepCommentBox); const slotProps = { data }; @@ -285,6 +288,7 @@ class Stream extends React.Component { )} )} + {changedUsername && } {!banned && rejectedUsername && } {banned && } {showCommentBox && ( diff --git a/locales/en.yml b/locales/en.yml index efcd7a1d9..eb472b373 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -254,6 +254,8 @@ en: error: "Usernames can contain letters numbers and _ only" label: "New Username" msg: "Your account is currently suspended because your username has been deemed inappropriate. To restore your account please enter a new username. Please contact us if you have any questions." + changed_name: + msg: "Your username change is under review by our moderation team." my_comments: "My Comments" my_profile: "My profile" new_count: "View {0} more {1}" diff --git a/test/e2e/specs/04_userStatus.js b/test/e2e/specs/04_userStatus.js index b63b051bb..8d659221d 100644 --- a/test/e2e/specs/04_userStatus.js +++ b/test/e2e/specs/04_userStatus.js @@ -15,13 +15,13 @@ module.exports = { client.end(); }, - 'admin logs in': client => { + 'Admin logs in': client => { const adminPage = client.page.admin(); const { testData: { admin } } = client.globals; adminPage.navigateAndLogin(admin); }, - 'admin flags users username as offensive': client => { + 'Admin flags users username as offensive': client => { const embedStream = client.page.embedStream(); const comments = embedStream.navigate().ready(); @@ -42,7 +42,7 @@ module.exports = { .waitForElementVisible('@popUpText') .click('@continueButton'); }, - 'admin goes to Reported Usernames': client => { + 'Admin goes to Reported Usernames': client => { const adminPage = client.page.admin(); const community = adminPage @@ -54,14 +54,14 @@ module.exports = { .waitForElementVisible('@flaggedAccountsContainer') .waitForElementVisible('@flaggedUser'); }, - 'admin rejects the user flag': client => { + 'Admin rejects the user flag': client => { const community = client.page.admin().section.community; community .waitForElementVisible('@flaggedUserRejectButton') .click('@flaggedUserRejectButton'); }, - 'admin suspends the user': client => { + 'Admin suspends the user': client => { const community = client.page.admin().section.community; const usernameDialog = client.page.admin().section.usernameDialog; @@ -76,7 +76,7 @@ module.exports = { community.waitForElementNotPresent('@flaggedUser'); }, - 'admin logs out': client => { + 'Admin logs out': client => { client.page.admin().logout(); }, 'user logs in': client => { @@ -114,6 +114,48 @@ module.exports = { .click('@changeUsernameSubmitButton') .waitForElementNotPresent('@changeUsernameInput'); }, + 'user should not be able to comment still': client => { + const embedStream = client.page.embedStream(); + const comments = embedStream.section.comments; + + comments + .waitForElementNotPresent('@commentBoxTextarea') + .waitForElementNotPresent('@commentBoxPostButton'); + }, + 'user logs out': client => { + const embedStream = client.page.embedStream(); + const comments = embedStream.section.comments; + + comments.logout(); + }, + 'Admin accepts the user flag': client => { + const adminPage = client.page.admin(); + const { testData: { admin } } = client.globals; + + adminPage.navigateAndLogin(admin); + + const community = adminPage + .navigate() + .ready() + .goToCommunity(); + + community + .waitForElementVisible('@flaggedAccountsContainer') + .waitForElementVisible('@flaggedUser') + .waitForElementVisible('@flaggedUserApproveButton') + .click('@flaggedUserApproveButton'); + + client.page.admin().logout(); + }, + 'user logs in to check comment': client => { + const { testData: { user } } = client.globals; + const embedStream = client.page.embedStream(); + + embedStream + .navigate() + .ready() + .openLoginPopup(popup => popup.login(user)); + }, 'user should be able to comment': client => { const embedStream = client.page.embedStream(); const comments = embedStream.section.comments; From efe84f3f983fa8e99baa3434a6d84cccae60faa2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 30 Jan 2018 14:50:35 +0100 Subject: [PATCH 16/16] Make Approve/Reject button styles more robust for long texts. For other languages than English the 'Approve' text did not fit into the button. This commit makes the CSS a bit more flexible by removing some very pixel-specfic styles. --- client/coral-admin/src/components/ApproveButton.css | 9 ++------- client/coral-admin/src/components/RejectButton.css | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/client/coral-admin/src/components/ApproveButton.css b/client/coral-admin/src/components/ApproveButton.css index 071be2fcb..db3ee63f6 100644 --- a/client/coral-admin/src/components/ApproveButton.css +++ b/client/coral-admin/src/components/ApproveButton.css @@ -5,16 +5,11 @@ background: white; padding: 10px 12px; box-sizing: border-box; - vertical-align: middle; - line-height: 24px; - font-size: 17px; - height: 47px; 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: 129px; - transform: scale(.8); - margin: 0; + width: 100%; + margin: 0 0 .5em; &:not(:disabled):hover { box-shadow: none; diff --git a/client/coral-admin/src/components/RejectButton.css b/client/coral-admin/src/components/RejectButton.css index 3885ebf9d..62a061fa4 100644 --- a/client/coral-admin/src/components/RejectButton.css +++ b/client/coral-admin/src/components/RejectButton.css @@ -5,16 +5,11 @@ background: white; padding: 10px 11px; box-sizing: border-box; - vertical-align: middle; - line-height: 24px; - font-size: 17px; - height: 47px; 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: 129px; - transform: scale(.8); - margin: 0; + width: 100%; + margin: 0 0 .5em; &:not(:disabled):hover { color: white;