From 7955b28a75f59b48e13609395c7df3e62e196a74 Mon Sep 17 00:00:00 2001 From: okbel Date: Wed, 4 Apr 2018 07:59:13 -0300 Subject: [PATCH 01/25] Adding rejectUsername --- client/coral-admin/src/components/UserDetail.js | 9 ++++++++- client/coral-admin/src/containers/UserDetail.js | 6 +++++- client/coral-framework/utils/user.js | 9 +++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 31ad999dc..b917d81eb 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -110,6 +110,7 @@ class UserDetail extends React.Component { unbanUser, unsuspendUser, modal, + rejectUsername, } = this.props; // if totalComments is 0, you're dividing by zero @@ -122,6 +123,8 @@ class UserDetail extends React.Component { const banned = isBanned(user); const suspended = isSuspended(user); + console.log(user); + const slotPassthrough = { root, user, @@ -155,6 +158,10 @@ class UserDetail extends React.Component { )} label={this.getActionMenuLabel()} > + rejectUsername({ id: user.id })}> + Reject Username + + {suspended ? ( unsuspendUser({ id: user.id })}> {t('user_detail.remove_suspension')} @@ -167,7 +174,6 @@ class UserDetail extends React.Component { {t('user_detail.suspend')} )} - {banned ? ( unbanUser({ id: user.id })}> {t('user_detail.remove_ban')} @@ -376,6 +382,7 @@ UserDetail.propTypes = { unbanUser: PropTypes.func.isRequired, unsuspendUser: PropTypes.func.isRequired, modal: PropTypes.bool, + rejectUsername: PropTypes.func.isRequired, }; export default UserDetail; diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index fb1508a2f..2046f9c74 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -26,6 +26,7 @@ import UserDetailComment from './UserDetailComment'; import update from 'immutability-helper'; import { showBanUserDialog } from 'actions/banUserDialog'; import { showSuspendUserDialog } from 'actions/suspendUserDialog'; +import { withRejectUsername } from '../../../coral-framework/graphql/mutations'; const commentConnectionFragment = gql` fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection { @@ -131,6 +132,7 @@ class UserDetailContainer extends React.Component { loading={loading} error={this.props.data && this.props.data.error} loadMore={this.loadMore} + rejectUsername={this.props.rejectUsername} {...this.props} /> ); @@ -148,6 +150,7 @@ UserDetailContainer.propTypes = { selectedCommentIds: PropTypes.array, unbanUser: PropTypes.func.isRequired, unsuspendUser: PropTypes.func.isRequired, + rejectUsername: PropTypes.func.isRequired, }; const LOAD_MORE_QUERY = gql` @@ -281,5 +284,6 @@ export default compose( withUserDetailQuery, withSetCommentStatus, withUnbanUser, - withUnsuspendUser + withUnsuspendUser, + withRejectUsername )(UserDetailContainer); diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index f50fca335..63a552110 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -33,3 +33,12 @@ export const isSuspended = user => { export const isBanned = user => { return get(user, 'state.status.banned.status'); }; + +/** + * isUsernameRejected + * retrieves boolean based on the username status + */ + +export const isUsernameRejected = user => { + return get(user, 'state.status.username.status'); +}; From 8563ddffd35fa91085742260654f0846a5a2f9a4 Mon Sep 17 00:00:00 2001 From: okbel Date: Wed, 11 Apr 2018 23:55:50 -0300 Subject: [PATCH 02/25] Adding functionality and design functionality --- .../coral-admin/src/components/ActionsMenu.js | 2 +- .../coral-admin/src/components/UserDetail.js | 75 +++++++++++++++---- .../coral-admin/src/containers/UserDetail.js | 6 +- client/coral-framework/utils/user.js | 30 +++++++- 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js index 81b857a13..52370f9a7 100644 --- a/client/coral-admin/src/components/ActionsMenu.js +++ b/client/coral-admin/src/components/ActionsMenu.js @@ -76,7 +76,7 @@ ActionsMenu.propTypes = { icon: PropTypes.string, children: PropTypes.node, className: PropTypes.string, - label: PropTypes.string, + label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), buttonClassNames: PropTypes.string, }; diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index b917d81eb..a6684a04d 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -10,6 +10,9 @@ import { getReliability, isSuspended, isBanned, + isUsernameRejected, + isUsernameChanged, + getActiveStatuses, } from 'coral-framework/utils/user'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; import ClickOutside from 'coral-framework/components/ClickOutside'; @@ -26,6 +29,7 @@ import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import UserInfoTooltip from './UserInfoTooltip'; import t from 'coral-framework/services/i18n'; +import flatten from 'lodash/flatten'; class UserDetail extends React.Component { rejectThenReload = async info => { @@ -84,18 +88,43 @@ class UserDetail extends React.Component { ); } - getActionMenuLabel() { - const { root: { user } } = this.props; + getActionMenuLabel(user) { + const activeStatuses = getActiveStatuses(user); + const count = activeStatuses.length; - if (isBanned(user)) { - return 'Banned'; - } else if (isSuspended(user)) { - return 'Suspended'; + if (count > 1) { + return `Status(${count})`; + } else { + const activeStatus = flatten(activeStatuses)[0]; + + switch (activeStatus) { + case 'suspended': + return Suspended; + case 'banned': + return Banned; + case 'usernameRejected': + return ( + + Username + + ); + case 'usernameChanged': + return ( + + Username + + ); + default: + return activeStatus; + } } - - return ''; } + goToReportedUsernames = () => { + const { router } = this.props; + router.push('/admin/community/flagged'); + }; + renderLoaded() { const { root, @@ -122,8 +151,8 @@ class UserDetail extends React.Component { const banned = isBanned(user); const suspended = isSuspended(user); - - console.log(user); + const usernameRejected = isUsernameRejected(user); + const usernameChanged = isUsernameChanged(user); const slotPassthrough = { root, @@ -156,12 +185,8 @@ class UserDetail extends React.Component { }, 'talk-admin-user-detail-actions-button' )} - label={this.getActionMenuLabel()} + label={this.getActionMenuLabel(user)} > - rejectUsername({ id: user.id })}> - Reject Username - - {suspended ? ( unsuspendUser({ id: user.id })}> {t('user_detail.remove_suspension')} @@ -174,6 +199,7 @@ class UserDetail extends React.Component { {t('user_detail.suspend')} )} + {banned ? ( unbanUser({ id: user.id })}> {t('user_detail.remove_ban')} @@ -186,6 +212,24 @@ class UserDetail extends React.Component { {t('user_detail.ban')} )} + + {usernameChanged && ( + + Username needs approval + + )} + + {usernameRejected && !usernameChanged ? ( + + Username Rejected + + ) : ( + rejectUsername({ id: user.id })} + > + Reject Username + + )} )} @@ -361,6 +405,7 @@ class UserDetail extends React.Component { } UserDetail.propTypes = { + router: PropTypes.object.isRequired, userId: PropTypes.string.isRequired, hideUserDetail: PropTypes.func.isRequired, root: PropTypes.object.isRequired, diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 2046f9c74..38a610880 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import UserDetail from '../components/UserDetail'; import withQuery from 'coral-framework/hocs/withQuery'; +import { withRouter } from 'react-router'; import { getDefinitionName, getSlotFragmentSpreads, @@ -21,12 +22,12 @@ import { withSetCommentStatus, withUnbanUser, withUnsuspendUser, + withRejectUsername, } from 'coral-framework/graphql/mutations'; import UserDetailComment from './UserDetailComment'; import update from 'immutability-helper'; import { showBanUserDialog } from 'actions/banUserDialog'; import { showSuspendUserDialog } from 'actions/suspendUserDialog'; -import { withRejectUsername } from '../../../coral-framework/graphql/mutations'; const commentConnectionFragment = gql` fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection { @@ -285,5 +286,6 @@ export default compose( withSetCommentStatus, withUnbanUser, withUnsuspendUser, - withRejectUsername + withRejectUsername, + withRouter )(UserDetailContainer); diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index 63a552110..c6b2ba83d 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -1,4 +1,6 @@ import get from 'lodash/get'; +import mapValues from 'lodash/mapValues'; +import toPairs from 'lodash/toPairs'; /** * getReliability @@ -40,5 +42,31 @@ export const isBanned = user => { */ export const isUsernameRejected = user => { - return get(user, 'state.status.username.status'); + return get(user, 'state.status.username.status') === 'REJECTED'; +}; + +/** + * isUsernameChanged + * retrieves boolean based on the username status + */ + +export const isUsernameChanged = user => { + return get(user, 'state.status.username.status') === 'CHANGED'; +}; + +/** + * getActiveStatuses + * returns an array of active status(es) + * i.e if suspension is active, it returns suspension + */ + +export const getActiveStatuses = user => { + const statusMap = { + suspended: isSuspended, + banned: isBanned, + usernameRejected: isUsernameRejected, + usernameChanged: isUsernameChanged, + }; + + return toPairs(mapValues(statusMap, fn => fn(user))).filter(x => x[1]); }; From 3d3a46070c43a737b898792ee375cf7ea6ea6969 Mon Sep 17 00:00:00 2001 From: okbel Date: Thu, 12 Apr 2018 00:09:33 -0300 Subject: [PATCH 03/25] Adding translations --- .../coral-admin/src/components/UserDetail.js | 18 +++++++------ locales/en.yml | 6 +++++ locales/es.yml | 25 +++++++++++-------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index a6684a04d..2476898a5 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -99,19 +99,23 @@ class UserDetail extends React.Component { switch (activeStatus) { case 'suspended': - return Suspended; + return t('user_detail.suspended'); case 'banned': - return Banned; + return t('user_detail.banned'); case 'usernameRejected': return ( - Username + {t('user_detail.username')} + {` `} + ); case 'usernameChanged': return ( - Username + {t('user_detail.username')} + {` `} + ); default: @@ -215,19 +219,19 @@ class UserDetail extends React.Component { {usernameChanged && ( - Username needs approval + {t('user_detail.username_needs_approval')} )} {usernameRejected && !usernameChanged ? ( - Username Rejected + {t('user_detail.username_rejected')} ) : ( rejectUsername({ id: user.id })} > - Reject Username + {t('user_detail.reject_username')} )} diff --git a/locales/en.yml b/locales/en.yml index 08d64ddc8..b63142e63 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -428,6 +428,12 @@ en: user_bio: "User Bio" username_flags: "flags for this username" user_detail: + suspended: 'Suspended' + banned: 'Banned' + username: 'Username' + username_needs_approval: 'Username needs approval' + username_rejected: 'Username rejected' + reject_username: 'Reject Username' remove_suspension: "Remove Suspension" suspend: "Suspend User" remove_ban: "Remove Ban" diff --git a/locales/es.yml b/locales/es.yml index ba6cb8ad3..b6b01e2b6 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -421,17 +421,22 @@ es: user_bio: "Bio de Usuario" username_flags: "reportes para este nombre de usuario" user_detail: - remove_suspension: "Remove Suspension" - suspend: "Suspend User" - remove_ban: "Remove Ban" - ban: "Ban User" - member_since: "Member Since" + suspended: 'Suspendido' + banned: 'Baneado' + username: 'Usuario' + username_needs_approval: 'El usuario necesita aprovación' + username_rejected: 'Usuario rechazado' + remove_suspension: "Quitar suspensión" + suspend: "Suspender usuario" + remove_ban: "Quitar ban" + ban: "Banear usuario" + member_since: "Miembro desde" email: "Email" - total_comments: "Total Comments" - reject_rate: "Reject Rate" - reports: "Reports" - all: "All" - rejected: "Rejected" + total_comments: "Comentarios totales" + reject_rate: "Promedio de rechazo" + reports: "Reportes" + all: "Todos" + rejected: "Rechazado" account_history: "Account History" account_history: user_banned: "User banned" From c2b14fca1097292ff3594bcfc04d7f4e8341b8b1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Apr 2018 14:34:39 -0600 Subject: [PATCH 04/25] fixed mutation --- client/coral-framework/graphql/mutations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 1b3b4c6fc..aea85c40f 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -271,7 +271,7 @@ export const withRejectUsername = withMutation( `, { props: ({ mutate }) => ({ - rejectUsername: id => { + rejectUsername: ({ id }) => { return mutate({ variables: { id, From 42ca5743aac3694497522c87c92fcb02ad860d9f Mon Sep 17 00:00:00 2001 From: okbel Date: Mon, 16 Apr 2018 16:07:00 -0300 Subject: [PATCH 05/25] flow checks --- client/coral-admin/src/components/ActionsMenuItem.js | 2 +- client/coral-admin/src/components/UserDetail.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js index 7370ba466..54816147a 100644 --- a/client/coral-admin/src/components/ActionsMenuItem.js +++ b/client/coral-admin/src/components/ActionsMenuItem.js @@ -15,7 +15,7 @@ const ActionsMenuItem = props => ( ActionsMenuItem.propTypes = { className: PropTypes.string, - children: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), }; export default ActionsMenuItem; diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 2476898a5..aed200f18 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -220,16 +220,19 @@ class UserDetail extends React.Component { {usernameChanged && ( {t('user_detail.username_needs_approval')} + {` `} + )} {usernameRejected && !usernameChanged ? ( - + {t('user_detail.username_rejected')} ) : ( rejectUsername({ id: user.id })} + disabled={me.id === user.id} > {t('user_detail.reject_username')} From 6d35c648d65e07849eb0b5eff77ae481a36990d1 Mon Sep 17 00:00:00 2001 From: okbel Date: Mon, 28 May 2018 12:13:02 -0300 Subject: [PATCH 06/25] updates --- client/coral-admin/src/components/UserDetail.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 450b2cecc..7a7d7a672 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -8,13 +8,12 @@ import { Slot } from 'coral-framework/components'; import UserDetailCommentList from '../components/UserDetailCommentList'; import { - getReliability, isSuspended, - isBanned, isUsernameRejected, isUsernameChanged, getActiveStatuses, - isSuspended, isBanned, getKarma + isBanned, + getKarma, } from 'coral-framework/utils/user'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; @@ -113,6 +112,13 @@ class UserDetail extends React.Component { router.push('/admin/community/flagged'); }; + rejectUsername = data => { + // trigger modal or tooltip + // flag user and then + // perform rejection + this.props.rejectUsername(data); + }; + renderLoaded() { const { root, @@ -133,7 +139,6 @@ class UserDetail extends React.Component { unbanUser, unsuspendUser, modal, - rejectUsername, acceptComment, rejectComment, bulkAccept, @@ -225,7 +230,7 @@ class UserDetail extends React.Component { ) : ( rejectUsername({ id: user.id })} + onClick={() => this.rejectUsername({ id: user.id })} disabled={me.id === user.id} > {t('user_detail.reject_username')} From b0d76f2f6a22524b22be1a12dba75fb9d1283350 Mon Sep 17 00:00:00 2001 From: okbel Date: Mon, 28 May 2018 14:52:03 -0300 Subject: [PATCH 07/25] progress --- .../src/actions/rejectUsernameDialog.js | 14 ++ .../src/components/RejectUsernameDialog.css | 90 +++++++++ .../src/components/RejectUsernameDialog.js | 188 ++++++++++++++++++ .../coral-admin/src/components/UserDetail.js | 8 +- .../src/constants/rejectUsernameDialog.js | 2 + client/coral-admin/src/containers/Layout.js | 2 + .../src/containers/RejectUsernameDialog.js | 79 ++++++++ .../coral-admin/src/containers/UserDetail.js | 4 + client/coral-admin/src/reducers/index.js | 2 + .../src/reducers/rejectUsernameDialog.js | 29 +++ 10 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 client/coral-admin/src/actions/rejectUsernameDialog.js create mode 100644 client/coral-admin/src/components/RejectUsernameDialog.css create mode 100644 client/coral-admin/src/components/RejectUsernameDialog.js create mode 100644 client/coral-admin/src/constants/rejectUsernameDialog.js create mode 100644 client/coral-admin/src/containers/RejectUsernameDialog.js create mode 100644 client/coral-admin/src/reducers/rejectUsernameDialog.js diff --git a/client/coral-admin/src/actions/rejectUsernameDialog.js b/client/coral-admin/src/actions/rejectUsernameDialog.js new file mode 100644 index 000000000..946b36ef7 --- /dev/null +++ b/client/coral-admin/src/actions/rejectUsernameDialog.js @@ -0,0 +1,14 @@ +import { + SHOW_REJECT_USERNAME_DIALOG, + HIDE_REJECT_USERNAME_DIALOG, +} from '../constants/rejectUsernameDialog'; + +export const showRejectUsernameDialog = ({ userId, username }) => ({ + type: SHOW_REJECT_USERNAME_DIALOG, + userId, + username, +}); + +export const hideRejectUsernameDialog = () => ({ + type: HIDE_REJECT_USERNAME_DIALOG, +}); diff --git a/client/coral-admin/src/components/RejectUsernameDialog.css b/client/coral-admin/src/components/RejectUsernameDialog.css new file mode 100644 index 000000000..1c96f509a --- /dev/null +++ b/client/coral-admin/src/components/RejectUsernameDialog.css @@ -0,0 +1,90 @@ +.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: 400px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; +} + +.header { + color: black; + font-size: 1.5em; + font-weight: 500; + margin: 0 0 8px 0; +} + +.close { + display: block; + position: absolute; + top: 24px; + right: 20px; +} + +.closeButton { + userSelect: none; + outline: none; + border: none; + touchAction: manipulation; + &::-moz-focus-inner: { + border: 0; + } + background: 0; + padding: 0; + font-size: 24px; + line-height: 14px; + cursor: pointer; + color: #363636; + &:hover { + color: #6b6b6b; + } +} + +.legend { + padding: 0; + font-weight: bold; +} + +div.radioGroup { + margin-top: 6px; +} + +label.radioGroup { + + &:global(.is-checked) > :global(.mdl-radio__outer-circle), + > :global(.mdl-radio__outer-circle) { + border-color: #212121; + } + + > :global(.mdl-radio__inner-circle) { + background: #212121; + } + + > :global(.mdl-radio__label) { + font-size: 14px; + line-height: 20px; + } +} + +.messageInput { + border-radius: 3px; + width: 100%; + padding: 10px; + font-size: 14px; + box-sizing: border-box; +} + +.cancel { + margin-right: 5px; +} + +.perform { + min-width: 90px; +} + +.buttons { + margin-top: 8px; + margin-bottom: 6px; + text-align: right; +} diff --git a/client/coral-admin/src/components/RejectUsernameDialog.js b/client/coral-admin/src/components/RejectUsernameDialog.js new file mode 100644 index 000000000..cdd211bc4 --- /dev/null +++ b/client/coral-admin/src/components/RejectUsernameDialog.js @@ -0,0 +1,188 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dialog } from 'coral-ui'; +import { RadioGroup, Radio } from 'react-mdl'; +import styles from './SuspendUserDialog.css'; +import cn from 'classnames'; + +import Button from 'coral-ui/components/Button'; + +import t, { timeago } from 'coral-framework/services/i18n'; +import { dateAdd } from 'coral-framework/utils'; + +const initialState = { step: 0, duration: '3' }; + +function durationsToDate(hours) { + // Add 1 minute more to help `timeago.js` to display the correct duration. + return dateAdd(new Date(), 'minute', hours * 60 + 1); +} + +class SuspendUserDialog extends React.Component { + state = initialState; + + componentWillReceiveProps(next) { + if (this.props.open && !next.open) { + this.setState(initialState); + } + } + + handleDurationChange = event => { + this.setState({ duration: event.target.value }); + }; + + handleMessageChange = event => { + this.setState({ message: event.target.value }); + }; + + goToStep1 = () => { + this.setState({ + step: 1, + message: t( + 'suspenduser.email_message_suspend', + this.props.username, + this.props.organizationName, + timeago(durationsToDate(this.state.duration)) + ), + }); + }; + + handlePerform = () => { + this.props.onPerform({ + message: this.state.message, + + // Add 1 minute more to help `timeago.js` to display the correct duration. + until: durationsToDate(this.state.duration), + }); + }; + + renderStep0() { + const { onCancel, username } = this.props; + const { duration } = this.state; + return ( +
+

{t('suspenduser.title_suspend')}

+

+ {t('suspenduser.description_suspend', username)} +

+
+ + {t('suspenduser.select_duration')} + + + {t('suspenduser.one_hour')} + {t('suspenduser.hours', 3)} + {t('suspenduser.hours', 24)} + {t('suspenduser.days', 7)} + +
+
+ + +
+
+ ); + } + + renderStep1() { + const { message } = this.state; + const { onCancel, username } = this.props; + return ( +
+

{t('suspenduser.title_notify')}

+

+ {t('suspenduser.description_notify', username)} +

+
+ + {t('suspenduser.write_message')} + +