diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
index 5df119fbc..e60756f39 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
@@ -27,6 +27,7 @@ import {
getActionSummary,
iPerformedThisAction,
isCommentActive,
+ isCommentDeleted,
getShallowChanges,
} from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
@@ -742,8 +743,14 @@ export default class Comment extends React.Component {
return (
- {this.renderComment()}
- {activeReplyBox === comment.id && this.renderReplyBox()}
+ {isCommentDeleted(comment) ? (
+
+ ) : (
+
+ {this.renderComment()}
+ {activeReplyBox === comment.id && this.renderReplyBox()}
+
+ )}
{this.renderRepliesContainer()}
);
diff --git a/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
index a10369850..95184dffd 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
@@ -13,6 +13,8 @@ class CommentTombstone extends React.Component {
return t('framework.comment_is_ignored');
case 'reject':
return t('framework.comment_is_rejected');
+ case 'deleted':
+ return t('framework.comment_is_deleted');
default:
return t('framework.comment_is_hidden');
}
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
index 66792b9b3..f69e83bdc 100644
--- a/client/coral-framework/utils/index.js
+++ b/client/coral-framework/utils/index.js
@@ -1,6 +1,7 @@
import { gql } from 'react-apollo';
import t from 'coral-framework/services/i18n';
import union from 'lodash/union';
+import get from 'lodash/get';
import { capitalize } from 'coral-framework/helpers/strings';
import assignWith from 'lodash/assignWith';
import mapValues from 'lodash/mapValues';
@@ -221,6 +222,13 @@ export function isCommentActive(commentStatus) {
return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0;
}
+export function isCommentDeleted(comment) {
+ return (
+ get(comment, 'body', null) === null ||
+ get(comment, 'deleted_at', null) !== null
+ );
+}
+
export function getShallowChanges(a, b) {
return union(Object.keys(a), Object.keys(b)).filter(key => a[key] !== b[key]);
}
diff --git a/graph/mutators/user.js b/graph/mutators/user.js
index 0343e1f2e..f5bf3c930 100644
--- a/graph/mutators/user.js
+++ b/graph/mutators/user.js
@@ -92,7 +92,7 @@ const delUser = async (ctx, id) => {
updateBatchSize: 10000,
});
- // Remove all actions against comments.
+ // Remove all actions against this users comments.
await transformSingleWithCursor(
Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
actionDecrTransformer,
@@ -111,34 +111,41 @@ const delUser = async (ctx, id) => {
.setOptions({ multi: true })
.remove();
- // Removes all the user's reply counts on each of the comments that they
- // have commented on.
+ // For each comment that the user has authored, purge the comment data from it
+ // and unset their id from those comments.
await transformSingleWithCursor(
- Comment.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,
- },
+ Comment.collection.find({ author_id: user.id }),
+ ({
+ id,
+ asset_id,
+ status,
+ parent_id,
+ reply_count,
+ created_at,
+ updated_at,
+ }) => ({
+ query: { id },
+ replace: {
+ id,
+ body: null,
+ body_history: [],
+ asset_id,
+ author_id: null,
+ status_history: [],
+ status,
+ parent_id,
+ reply_count,
+ action_counts: {},
+ tags: [],
+ metadata: {},
+ deleted_at: new Date(),
+ created_at,
+ updated_at,
},
}),
Comment
);
- // Remove all the user's comments.
- await Comment.where({ author_id: user.id })
- .setOptions({ multi: true })
- .remove();
-
// Remove the user.
await user.remove();
};
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index 064835f8f..e562c062c 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -4,6 +4,7 @@ const {
SEARCH_ACTIONS,
SEARCH_COMMENT_STATUS_HISTORY,
VIEW_BODY_HISTORY,
+ VIEW_COMMENT_DELETED_AT,
} = require('../../perms/constants');
const {
decorateWithTags,
@@ -23,7 +24,9 @@ const Comment = {
return Comments.get.load(parent_id);
},
user({ author_id }, _, { loaders: { Users } }) {
- return Users.getByID.load(author_id);
+ if (author_id) {
+ return Users.getByID.load(author_id);
+ }
},
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
// Don't bother looking up replies if there aren't any there!
@@ -83,6 +86,7 @@ decorateWithTags(Comment);
decorateWithPermissionCheck(Comment, {
actions: [SEARCH_ACTIONS],
status_history: [SEARCH_COMMENT_STATUS_HISTORY],
+ deleted_at: [VIEW_COMMENT_DELETED_AT],
});
// Protect privileged fields.
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index f94322344..9a03e2729 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -503,7 +503,7 @@ type Comment {
id: ID!
# The actual comment data.
- body: String!
+ body: String
# The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or
# the author.
@@ -537,6 +537,9 @@ type Comment {
# The status history of the comment. Requires the `ADMIN` or `MODERATOR` role.
status_history: [CommentStatusHistory!]
+ # The date that the comment was deleted at if it was.
+ deleted_at: Date
+
# The time when the comment was created
created_at: Date!
diff --git a/locales/en.yml b/locales/en.yml
index 50845b96f..f4c477710 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -258,6 +258,7 @@ en:
comment: comment
comment_is_ignored: "This comment is hidden because you ignored this user."
comment_is_rejected: "You have rejected this comment."
+ comment_is_deleted: "This comment was deleted."
comment_is_hidden: "This comment is not available."
comments: comments
configure_stream: "Configure"
diff --git a/models/schema/comment.js b/models/schema/comment.js
index 6ef5434d5..985f5c399 100644
--- a/models/schema/comment.js
+++ b/models/schema/comment.js
@@ -58,8 +58,6 @@ const Comment = new Schema(
},
body: {
type: String,
- required: [true, 'The body is required.'],
- minlength: 2,
},
body_history: [BodyHistoryItemSchema],
asset_id: String,
@@ -89,6 +87,12 @@ const Comment = new Schema(
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
+ // deleted_at stores the date that the given comment was deleted.
+ deleted_at: {
+ type: Date,
+ default: null,
+ },
+
// Additional metadata stored on the field.
metadata: {
default: {},
diff --git a/perms/constants/query.js b/perms/constants/query.js
index 197c5d9f9..0c7f4024d 100644
--- a/perms/constants/query.js
+++ b/perms/constants/query.js
@@ -11,4 +11,5 @@ module.exports = {
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
VIEW_BODY_HISTORY: 'VIEW_BODY_HISTORY',
+ VIEW_COMMENT_DELETED_AT: 'VIEW_COMMENT_DELETED_AT',
};
diff --git a/perms/reducers/query.js b/perms/reducers/query.js
index ed507139d..5c8b17307 100644
--- a/perms/reducers/query.js
+++ b/perms/reducers/query.js
@@ -14,6 +14,7 @@ module.exports = (user, perm) => {
case types.VIEW_USER_ROLE:
case types.VIEW_USER_EMAIL:
case types.VIEW_BODY_HISTORY:
+ case types.VIEW_COMMENT_DELETED_AT:
return check(user, ['ADMIN', 'MODERATOR']);
case types.LIST_OWN_TOKENS:
return check(user, ['ADMIN']);
diff --git a/services/migration/helpers.js b/services/migration/helpers.js
index 5eb33aadf..49acc78aa 100644
--- a/services/migration/helpers.js
+++ b/services/migration/helpers.js
@@ -10,8 +10,14 @@ const processUpdates = async (model, updates) => {
// Create a new batch operation.
const bulk = model.collection.initializeUnorderedBulkOp();
- for (const { query, update } of updates) {
- bulk.find(query).updateOne(update);
+ for (const { query, update, replace } of updates) {
+ if (update) {
+ bulk.find(query).updateOne(update);
+ } else if (replace) {
+ bulk.find(query).replaceOne(replace);
+ } else {
+ throw new Error('invalid update object provided');
+ }
}
// Execute the bulk update operation.