diff --git a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js
index f3daeffc2..3505ef7b5 100644
--- a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js
@@ -57,7 +57,7 @@ class OrganizationSettings extends React.Component {
}
const updater = { organizationContactEmail: { $set: email } };
- const errorUpdater = { organizationEmail: { $set: error } };
+ const errorUpdater = { organizationContactEmail: { $set: error } };
this.props.updatePending({ updater, errorUpdater });
};
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 0983b11c4..8b1524839 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';
@@ -744,8 +745,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-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
index 0a7a1951c..17d3132ea 100644
--- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
@@ -372,6 +372,7 @@ const slots = [
'streamTabsPrepend',
'streamTabPanes',
'streamFilter',
+ 'stream',
];
const fragments = {
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
index 8dc8fe113..e30c12e06 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 04fdcd366..8657905d9 100644
--- a/graph/mutators/user.js
+++ b/graph/mutators/user.js
@@ -8,7 +8,7 @@ const {
SET_USER_BAN_STATUS,
SET_USER_SUSPENSION_STATUS,
UPDATE_USER_ROLES,
- DELETE_USER,
+ DELETE_OTHER_USER,
CHANGE_PASSWORD,
} = require('../../perms/constants');
@@ -93,7 +93,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,
@@ -112,34 +112,50 @@ 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.
+ // Remove the user from all other user's ignore lists.
+ await User.update(
+ { ignoresUsers: user.id },
+ {
+ $pull: { ignoresUsers: user.id },
+ },
+ { multi: true }
+ );
+
+ // 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();
};
@@ -225,7 +241,7 @@ module.exports = ctx => {
setUserSuspensionStatus(ctx, id, until, message);
}
- if (ctx.user.can(DELETE_USER)) {
+ if (ctx.user.can(DELETE_OTHER_USER)) {
mutators.User.del = id => delUser(ctx, id);
}
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 03b199e0e..b5c193e3d 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 045d37c46..3d2fe88c7 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -252,6 +252,7 @@ en:
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
INCORRECT_PASSWORD: "Incorrect Password"
email: "Please enter a valid email."
+ DELETION_NOT_SCHEDULED: "Deletion was not scheduled"
confirm_password: "Passwords don't match. Please check again"
network_error: "Failed to connect to server. Check your internet connection and try again."
email_not_verified: "Email address {0} not verified."
@@ -272,6 +273,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 d9586e850..a211884ff 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/package.json b/package.json
index a1eb815be..1127dd05c 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"lint:js": "eslint bin/cli* .",
"lint": "npm-run-all lint:*",
"plugins:reconcile": "./bin/cli plugins reconcile",
- "test": "npm-run-all test:jest test:mocha",
+ "test": "npm-run-all test:jest test:server:mocha",
"test:jest": "NODE_ENV=test jest --runInBand",
"test:client": "NODE_ENV=test jest --projects client",
"test:server": "npm-run-all test:server:jest test:server:mocha",
diff --git a/perms/constants/mutation.js b/perms/constants/mutation.js
index 57aa254e7..2d4ae04cb 100644
--- a/perms/constants/mutation.js
+++ b/perms/constants/mutation.js
@@ -18,6 +18,6 @@ module.exports = {
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
- DELETE_USER: 'DELETE_USER',
+ DELETE_OTHER_USER: 'DELETE_OTHER_USER',
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
};
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/mutation.js b/perms/reducers/mutation.js
index 44f66cf88..8133fcd47 100644
--- a/perms/reducers/mutation.js
+++ b/perms/reducers/mutation.js
@@ -56,6 +56,7 @@ module.exports = (user, perm) => {
case types.UPDATE_USER_ROLES:
case types.CREATE_TOKEN:
case types.REVOKE_TOKEN:
+ case types.DELETE_OTHER_USER:
return check(user, ['ADMIN']);
default:
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/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js
index 3fe742569..aeb9841f9 100644
--- a/plugin-api/beta/client/utils/index.js
+++ b/plugin-api/beta/client/utils/index.js
@@ -8,4 +8,5 @@ export {
getErrorMessages,
getDefinitionName,
getShallowChanges,
+ createDefaultResponseFragments,
} from 'coral-framework/utils';
diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js
index e90a06122..5aa3063b3 100644
--- a/plugin-api/beta/server/getReactionConfig.js
+++ b/plugin-api/beta/server/getReactionConfig.js
@@ -6,6 +6,7 @@ const { CREATE_MONGO_INDEXES } = require('../../../config');
const Comment = require('models/comment');
function getReactionConfig(reaction) {
+ // Ensure that the reaction is a lowercase string.
reaction = reaction.toLowerCase();
if (CREATE_MONGO_INDEXES) {
diff --git a/plugins/talk-plugin-profile-data/client/components/AccountDeletionRequestedSign.css b/plugins/talk-plugin-profile-data/client/components/AccountDeletionRequestedSign.css
new file mode 100644
index 000000000..0dada08b5
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/AccountDeletionRequestedSign.css
@@ -0,0 +1,44 @@
+.container {
+ border: 1px solid #f26563;
+ border-radius: 2px;
+ color: #3b4a53;
+ padding: 20px 10px;
+ background-color: rgba(242, 101, 99, 0.1);
+ margin: 20px 0;
+}
+
+.button {
+ color: #787D80;
+ border-radius: 2px;
+ background-color: transparent;
+ height: 30px;
+ font-size: 0.9em;
+ line-height: normal;
+
+ &:hover {
+ background-color: #eaeaea;
+ }
+
+ &.secondary {
+ background-color: #787D80;
+ color: white;
+ }
+}
+
+.title {
+ margin: 0;
+ i.icon {
+ font-size: 1em;
+ padding: 4px;
+ }
+}
+
+.description {
+ margin: 0;
+ padding-left: 22px;
+ padding-bottom: 15px;
+}
+
+.actions {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-profile-data/client/components/AccountDeletionRequestedSign.js b/plugins/talk-plugin-profile-data/client/components/AccountDeletionRequestedSign.js
new file mode 100644
index 000000000..38d968c1d
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/AccountDeletionRequestedSign.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import { t } from 'plugin-api/beta/client/services';
+import moment from 'moment';
+import { Button, Icon } from 'plugin-api/beta/client/components/ui';
+import styles from './AccountDeletionRequestedSign.css';
+import { getErrorMessages } from 'coral-framework/utils';
+import { scheduledDeletionDelayHours } from '../../config';
+
+class AccountDeletionRequestedSign extends React.Component {
+ cancelAccountDeletion = async () => {
+ const { cancelAccountDeletion, notify } = this.props;
+ try {
+ await cancelAccountDeletion();
+ notify('success', t('delete_request.account_deletion_cancelled'));
+ } catch (err) {
+ notify('error', getErrorMessages(err));
+ }
+ };
+
+ render() {
+ const { me: { scheduledDeletionDate } } = this.props.root;
+
+ const deletionScheduledFor = moment(scheduledDeletionDate).format(
+ 'MMM Do YYYY, h:mm a'
+ );
+ const deletionScheduledOn = moment(scheduledDeletionDate)
+ .subtract(scheduledDeletionDelayHours, 'hours')
+ .format('MMM Do YYYY, h:mm a');
+
+ return (
+
+
+ {' '}
+ {t('delete_request.account_deletion_requested')}
+
+
+ {t('delete_request.received_on')}
+ {deletionScheduledFor}.
+
+
+ {t('delete_request.cancel_request_description')}
+
+ {' '}
+ {t('delete_request.before')} {deletionScheduledOn}
+ .
+
+
+
+
+
+ );
+ }
+}
+
+AccountDeletionRequestedSign.propTypes = {
+ cancelAccountDeletion: PropTypes.func.isRequired,
+ notify: PropTypes.func.isRequired,
+ root: PropTypes.object.isRequired,
+};
+
+export default AccountDeletionRequestedSign;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.css b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.css
new file mode 100644
index 000000000..e524d990a
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.css
@@ -0,0 +1,23 @@
+.button {
+ color: #787D80;
+ border-radius: 2px;
+ border: 1px solid #787d80;
+ background-color: transparent;
+ height: 30px;
+ font-size: 0.9em;
+ line-height: normal;
+
+ &:hover {
+ background-color: #eaeaea;
+ }
+
+ &.secondary {
+ background-color: #787D80;
+ color: white;
+ }
+
+ > i {
+ font-size: 1.2em;
+ vertical-align: middle;
+ }
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js
new file mode 100644
index 000000000..bbaca5b96
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js
@@ -0,0 +1,118 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import moment from 'moment';
+import styles from './DeleteMyAccount.css';
+import { Button } from 'plugin-api/beta/client/components/ui';
+import DeleteMyAccountDialog from './DeleteMyAccountDialog';
+import { getErrorMessages } from 'coral-framework/utils';
+import { t } from 'plugin-api/beta/client/services';
+
+const initialState = { showDialog: false };
+
+class DeleteMyAccount extends React.Component {
+ state = initialState;
+
+ showDialog = () => {
+ this.setState({
+ showDialog: true,
+ });
+ };
+
+ closeDialog = () => {
+ this.setState({
+ showDialog: false,
+ });
+ };
+
+ cancelAccountDeletion = async () => {
+ const { cancelAccountDeletion, notify } = this.props;
+ try {
+ await cancelAccountDeletion();
+ notify('success', t('delete_request.account_deletion_requested'));
+ } catch (err) {
+ notify('error', getErrorMessages(err));
+ }
+ };
+
+ requestAccountDeletion = async () => {
+ const { requestAccountDeletion, notify } = this.props;
+ try {
+ await requestAccountDeletion();
+ notify('success', t('delete_request.account_deletion_requested'));
+ } catch (err) {
+ notify('error', getErrorMessages(err));
+ }
+ };
+
+ render() {
+ const {
+ me: { scheduledDeletionDate },
+ settings: { organizationContactEmail },
+ } = this.props.root;
+ return (
+
+
+
+ {t('delete_request.delete_my_account')}
+
+
+ {t('delete_request.delete_my_account_description')}
+
+
+ {scheduledDeletionDate &&
+ t(
+ 'delete_request.already_submitted_request_description',
+ moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a')
+ )}
+
+ {scheduledDeletionDate ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+}
+
+DeleteMyAccount.propTypes = {
+ requestAccountDeletion: PropTypes.func.isRequired,
+ cancelAccountDeletion: PropTypes.func.isRequired,
+ notify: PropTypes.func.isRequired,
+ root: PropTypes.object.isRequired,
+};
+
+export default DeleteMyAccount;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountDialog.css b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountDialog.css
new file mode 100644
index 000000000..2735474da
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountDialog.css
@@ -0,0 +1,38 @@
+.dialog {
+ border: none;
+ box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
+ width: 380px;
+ top: 10px;
+ font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
+ font-size: 14px;
+ border-radius: 4px;
+ padding: 20px;
+}
+
+.close {
+ font-size: 20px;
+ line-height: 14px;
+ top: 10px;
+ right: 10px;
+ position: absolute;
+ display: block;
+ font-weight: bold;
+ color: #363636;
+ cursor: pointer;
+
+ &:hover {
+ color: #6b6b6b;
+ }
+}
+
+.title {
+ font-size: 1.2em;
+ margin-top: 0;
+ margin-bottom: 8px;
+}
+
+.description {
+ font-size: 1em;
+ line-height: 20px;
+ margin: 0;
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountDialog.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountDialog.js
new file mode 100644
index 000000000..9264bb75c
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountDialog.js
@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './DeleteMyAccountDialog.css';
+import { Dialog } from 'plugin-api/beta/client/components/ui';
+import StepProgress from './StepProgress';
+import DeleteMyAccountStep0 from './DeleteMyAccountStep0';
+import DeleteMyAccountStep1 from './DeleteMyAccountStep1';
+import DeleteMyAccountStep2 from './DeleteMyAccountStep2';
+import DeleteMyAccountStep3 from './DeleteMyAccountStep3';
+import DeleteMyAccountFinalStep from './DeleteMyAccountFinalStep';
+import { t } from 'plugin-api/beta/client/services';
+
+const initialState = { step: 0, formData: {} };
+
+class DeleteMyAccountDialog extends React.Component {
+ state = initialState;
+
+ goToNextStep = () => {
+ this.setState(state => ({
+ step: state.step + 1,
+ }));
+ };
+
+ clear = () => {
+ this.setState(initialState);
+ };
+
+ cancel = () => {
+ this.clear();
+ this.props.closeDialog();
+ };
+
+ onChange = e => {
+ const { name, value } = e.target;
+
+ this.setState(state => ({
+ formData: {
+ ...state.formData,
+ [name]: value,
+ },
+ }));
+ };
+
+ render() {
+ const { step } = this.state;
+ const { scheduledDeletionDate, organizationContactEmail } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+DeleteMyAccountDialog.propTypes = {
+ showDialog: PropTypes.bool.isRequired,
+ closeDialog: PropTypes.func.isRequired,
+ requestAccountDeletion: PropTypes.func.isRequired,
+ scheduledDeletionDate: PropTypes.any.isRequired,
+ organizationContactEmail: PropTypes.string.isRequired,
+};
+
+export default DeleteMyAccountDialog;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountFinalStep.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountFinalStep.js
new file mode 100644
index 000000000..289cd6c17
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountFinalStep.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import cn from 'classnames';
+import PropTypes from 'prop-types';
+import { Button, Icon } from 'plugin-api/beta/client/components/ui';
+import styles from './DeleteMyAccountStep.css';
+import { t } from 'plugin-api/beta/client/services';
+import moment from 'moment';
+
+const DeleteMyAccountFinalStep = props => (
+
+
+ {t('delete_request.your_request_submitted_description')}
+
+
+
+
+ {t('delete_request.your_account_deletion_scheduled')}
+
+
+
+
+ {moment(props.scheduledDeletionDate).format('MMM Do YYYY, h:mm a')}
+
+
+
+
+
+ {t('delete_request.changed_your_mind')}{' '}
+ {t('delete_request.simply_go_to')} “
+ {t('delete_request.cancel_account_deletion_request')}.
+ ”
+
+
+
+ {t('delete_request.tell_us_why')}.{' '}
+ {t('delete_request.feedback_copy')}{' '}
+
+ {props.organizationContactEmail}
+ .
+
+
+
+
+
+
+);
+
+DeleteMyAccountFinalStep.propTypes = {
+ finish: PropTypes.func.isRequired,
+ scheduledDeletionDate: PropTypes.any.isRequired,
+ organizationContactEmail: PropTypes.string.isRequired,
+};
+
+export default DeleteMyAccountFinalStep;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep.css b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep.css
new file mode 100644
index 000000000..7d02b4e6f
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep.css
@@ -0,0 +1,116 @@
+.list {
+ padding: 0;
+ margin: 20px 0;
+ list-style: none;
+}
+
+.item {
+ display: flex;
+ margin-bottom: 20px;
+
+ .itemIcon {
+ flex-grow: 0;
+ }
+
+ .text {
+ flex-grow: 1;
+ padding-left: 10px;
+ }
+
+ > i.itemIcon {
+ font-size: 1.3em;
+ }
+}
+
+.button {
+ color: #787D80;
+ border-radius: 2px;
+ background-color: transparent;
+ height: 30px;
+ font-size: 0.9em;
+ line-height: normal;
+
+ &:hover {
+ background-color: #eaeaea;
+ }
+
+ &.cancel {
+ background-color: transparent;
+ color: #787D80;
+ }
+
+ &.proceed {
+ background-color: #3498DB;
+ color: white;
+ }
+
+ &.danger {
+ background-color: #FA4643;
+ color: white;
+ }
+}
+
+.actions {
+ text-align: right;
+ padding-top: 20px;
+
+ &.columnView {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+.title {
+ font-size: 1.2em;
+ margin-bottom: 8px;
+}
+
+.description {
+ font-size: 1em;
+ line-height: 20px;
+ margin: 0;
+ margin-bottom: 15px;
+}
+
+.box {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 15px;
+}
+
+.note {
+ margin: 10px 0;
+}
+
+.subTitle {
+ font-size: 1em;
+ margin-bottom: 8px;
+}
+
+.textBox {
+ background-color: #F1F2F2;
+ border: none;
+ width: 100%;
+ padding: 15px;
+ box-sizing: border-box;
+ color: #3B4A53;
+ font-size: 1em;
+ margin-bottom: 15px;
+}
+
+.block {
+ display: block;
+ margin-top: 2px;
+}
+
+.step {
+ padding-top: 20px;
+}
+
+.scheduledDeletion {
+ i.timeIcon {
+ font-size: 1.2em;
+ padding: 4px;
+ }
+}
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep0.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep0.js
new file mode 100644
index 000000000..42cb3d5a3
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep0.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import { Button, Icon } from 'plugin-api/beta/client/components/ui';
+import styles from './DeleteMyAccountStep.css';
+import { t } from 'plugin-api/beta/client/services';
+
+const DeleteMyAccountStep0 = props => (
+
+
+ {t('delete_request.step_0.you_are_attempting')}
+
+
+ -
+
+ {t('delete_request.step_0.item_1')}
+
+ -
+
+ {t('delete_request.step_0.item_2')}
+
+ -
+
+ {t('delete_request.step_0.item_3')}
+
+
+
+
+
+
+
+);
+
+DeleteMyAccountStep0.propTypes = {
+ goToNextStep: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+};
+
+export default DeleteMyAccountStep0;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep1.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep1.js
new file mode 100644
index 000000000..3d069cfd1
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep1.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import { Button } from 'plugin-api/beta/client/components/ui';
+import styles from './DeleteMyAccountStep.css';
+import { t } from 'plugin-api/beta/client/services';
+import { scheduledDeletionDelayHours } from '../../config';
+
+const DeleteMyAccountStep1 = props => (
+
+
{t('delete_request.step_1.subtitle')}
+
+ {t('delete_request.step_1.description', scheduledDeletionDelayHours)}
+
+
{t('delete_request.step_1.subtitle_2')}
+
+ {t('delete_request.step_1.description_2', scheduledDeletionDelayHours)}
+
+
+
+
+
+
+);
+
+DeleteMyAccountStep1.propTypes = {
+ goToNextStep: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+};
+
+export default DeleteMyAccountStep1;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep2.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep2.js
new file mode 100644
index 000000000..c96405f52
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep2.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import { Button } from 'plugin-api/beta/client/components/ui';
+import styles from './DeleteMyAccountStep.css';
+import { t } from 'plugin-api/beta/client/services';
+
+const DeleteMyAccountStep2 = props => (
+
+
+ {t('delete_request.step_2.description')}
+
+
+ {t('delete_request.step_2.to_download')}
+
+ {t('delete_request.step_2.path')}
+
+
+
+
+
+
+
+);
+
+DeleteMyAccountStep2.propTypes = {
+ goToNextStep: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+};
+
+export default DeleteMyAccountStep2;
diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep3.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep3.js
new file mode 100644
index 000000000..31ed5c1b1
--- /dev/null
+++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccountStep3.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import { Button } from 'plugin-api/beta/client/components/ui';
+import styles from './DeleteMyAccountStep.css';
+import InputField from './InputField';
+import { t } from 'plugin-api/beta/client/services';
+
+const initialState = {
+ showError: false,
+};
+
+class DeleteMyAccountStep3 extends React.Component {
+ state = initialState;
+ phrase = 'delete';
+
+ showError = () => {
+ this.setState({
+ showError: true,
+ });
+ };
+
+ clear = () => {
+ this.setState(initialState);
+ };
+
+ deleteAndContinue = async () => {
+ if (this.formHasError()) {
+ this.showError();
+ return;
+ }
+
+ await this.props.requestAccountDeletion();
+ this.clear();
+ this.props.goToNextStep();
+ };
+
+ formHasError = () =>
+ !this.props.formData.confirmPhrase ||
+ this.props.formData.confirmPhrase !== this.phrase;
+
+ render() {
+ return (
+
+
+ {t('delete_request.step_3.subtitle')}
+
+
+ {t('delete_request.step_3.description')}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+DeleteMyAccountStep3.propTypes = {
+ goToNextStep: PropTypes.func.isRequired,
+ requestAccountDeletion: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ formData: PropTypes.object.isRequired,
+};
+
+export default DeleteMyAccountStep3;
diff --git a/plugins/talk-plugin-profile-data/client/components/DownloadCommentHistory.js b/plugins/talk-plugin-profile-data/client/components/DownloadCommentHistory.js
index 06fa1fa89..7af15b774 100644
--- a/plugins/talk-plugin-profile-data/client/components/DownloadCommentHistory.js
+++ b/plugins/talk-plugin-profile-data/client/components/DownloadCommentHistory.js
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { t } from 'plugin-api/beta/client/services';
import { Button } from 'plugin-api/beta/client/components/ui';
import styles from './DownloadCommentHistory.css';
+import { getErrorMessages } from 'coral-framework/utils';
+import { downloadRateLimitDays } from '../../config';
export const readableDuration = durAsHours => {
const durAsDays = Math.ceil(durAsHours / 24);
@@ -19,21 +21,30 @@ export const readableDuration = durAsHours => {
class DownloadCommentHistory extends Component {
static propTypes = {
requestDownloadLink: PropTypes.func.isRequired,
+ notify: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
};
+ requestDownloadLink = async () => {
+ const { requestDownloadLink, notify } = this.props;
+ try {
+ await requestDownloadLink();
+ notify('success', t('download_request.download_preparing'));
+ } catch (err) {
+ notify('error', getErrorMessages(err));
+ }
+ };
+
render() {
- const {
- root: { me: { lastAccountDownload } },
- requestDownloadLink,
- } = this.props;
+ const { root: { me: { lastAccountDownload } } } = this.props;
const now = new Date();
const lastAccountDownloadDate =
lastAccountDownload && new Date(lastAccountDownload);
const hoursLeft = lastAccountDownloadDate
? Math.ceil(
- 7 * 24 - (now.getTime() - lastAccountDownloadDate.getTime()) / 3.6e6
+ downloadRateLimitDays * 24 -
+ (now.getTime() - lastAccountDownloadDate.getTime()) / 3.6e6
)
: 0;
const canRequestDownload = !lastAccountDownloadDate || hoursLeft <= 0;
@@ -43,7 +54,7 @@ class DownloadCommentHistory extends Component {
{t('download_request.section_title')}
{t('download_request.you_will_get_a_copy')}{' '}
- {t('download_request.download_rate')}.
+ {t('download_request.download_rate', downloadRateLimitDays)}.
{lastAccountDownloadDate && (
@@ -52,7 +63,7 @@ class DownloadCommentHistory extends Component {
)}
{canRequestDownload ? (
-