diff --git a/bin/cli-users b/bin/cli-users index 012eba9d4..adfe3bf23 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -219,6 +219,7 @@ function listUsers() { 'Display Name', 'Profiles', 'Roles', + 'Status', 'State' ] }); @@ -229,6 +230,7 @@ function listUsers() { user.displayName, user.profiles.map((p) => p.provider).join(', '), user.roles.join(', '), + user.status, user.disabled ? 'Disabled' : 'Enabled' ]); }); @@ -296,6 +298,40 @@ function removeRole(userID, role) { }); } +/** + * Ban a user + * @param {String} userID id of the user to ban + */ +function ban(userID) { + User + .setStatus(userID, 'banned', '') + .then(() => { + console.log(`Banned the User ${userID}.`); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); +} + +/** + * Unban a user + * @param {String} userUD id of the user to remove the role from + */ +function unban(userID) { + User + .setStatus(userID, 'active', '') + .then(() => { + console.log(`Unban the User ${userID}.`); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); +} + /** * Disable a given user. * @param {String} userID the ID of a user to disable @@ -384,6 +420,16 @@ program .description('removes a role from a given user') .action(removeRole); +program + .command('ban ') + .description('ban a given user') + .action(ban); + +program + .command('uban ') + .description('unban a given user') + .action(unban); + program .command('disable ') .description('disable a given user from logging in') diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index e4a55a893..d4aee034e 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -1,4 +1,3 @@ - /** * Action disptacher related to comments */ @@ -16,3 +15,16 @@ export const flagComment = id => (dispatch, getState) => { export const createComment = (name, body) => dispatch => { dispatch({type: 'COMMENT_CREATE', name, body}); }; + +// Dialog Actions +export const showBanUserDialog = (userId, userName, commentId) => { + return dispatch => { + dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId}); + }; +}; + +export const hideBanUserDialog = (showDialog) => { + return dispatch => { + dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog}); + }; +}; diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 8b8e883d8..c4712835a 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -6,14 +6,15 @@ import { FETCH_COMMENTERS_FAILURE, SORT_UPDATE, COMMENTERS_NEW_PAGE, - SET_ROLE + SET_ROLE, + SET_COMMENTER_STATUS } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; export const fetchCommenters = (query = {}) => dispatch => { dispatch(requestFetchCommenters()); - coralApi(`/user?${qs.stringify(query)}`) + coralApi(`/users?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) => dispatch({ type: FETCH_COMMENTERS_SUCCESS, @@ -41,8 +42,15 @@ export const newPage = () => ({ }); export const setRole = (id, role) => dispatch => { - return coralApi(`/user/${id}/role`, {method: 'POST', body: {role}}) + return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); }; + +export const setCommenterStatus = (id, status) => dispatch => { + return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}}) + .then(() => { + return dispatch({type: SET_COMMENTER_STATUS, id, status}); + }); +}; diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js new file mode 100644 index 000000000..f2ff37cbd --- /dev/null +++ b/client/coral-admin/src/actions/users.js @@ -0,0 +1,14 @@ + +/** + * Action disptacher related to users + */ +// +// export const banUser = (status, author_id) => (dispatch) => { +// dispatch({type: 'USER_STATUS_UPDATE', author_id, status}); +// }; +export const banUser = (status, userId, commentId) => { + return dispatch => { + dispatch({type: 'USER_BAN', status, userId, commentId}); + dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'}); + }; +}; diff --git a/client/coral-admin/src/components/BanUserDialog.css b/client/coral-admin/src/components/BanUserDialog.css new file mode 100644 index 000000000..dfac4f194 --- /dev/null +++ b/client/coral-admin/src/components/BanUserDialog.css @@ -0,0 +1,147 @@ +.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: 280px; + top: 10px; +} + +.header { + margin-bottom: 20px; +} + +.header h1, .separator h1{ + text-align: center; + font-size: 1.2em; +} + +.formField { + margin-top: 15px; +} + +.formField label { + font-size: 1.08em; + font-weight: bold; + margin-bottom: 5px; +} + +.formField input { + width: 100%; + display: block; + border: none; + outline: none; + border: 1px solid rgba(0,0,0,.12); + padding: 10px 6px; + box-sizing: border-box; + border-radius: 2px; + margin: 5px auto; +} + +.footer { + margin: 20px auto 10px; + text-align: center; +} + +.footer span { + display: block; + margin-bottom: 5px; +} + +.footer a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; +} + +.socialConnections { + margin-bottom: 20px; +} + +.signInButton { + margin-top: 10px; +} + +.close { + font-size: 20px; + line-height: 14px; + top: 10px; + right: 10px; + position: absolute; + display: block; + font-weight: bold; + color: #363636; + cursor: pointer; +} + +.close:hover { + color: #6b6b6b; +} + +input.error{ + border: solid 2px #f44336; +} + +.errorMsg, .hint { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} + +.alert { + padding: 10px; + margin-bottom: 20px; + border-radius: 2px; +} + +.alert--success { + border: solid 1px #1ec00e; + background: #cbf1b8; + color: #006900; +} + +.alert--error { + background: #FFEBEE; + color: #B71C1C; +} + +.userBox a { + color: #2c69b6; + cursor: pointer; + margin: 0px; +} + +.attention { + display: inline-block; + width: 15px; + height: 15px; + background: #B71C1C; + color: #FFEBEE; + font-weight: bolder; + padding: 4px; + vertical-align: middle; + border-radius: 20px; + box-sizing: border-box; + font-size: 9px; + line-height: 7px; + text-align: center; + margin-right: 5px; +} + +.action { + margin-top: 15px; +} + +.passwordRequestSuccess { + border: 1px solid green; + background-color: lightgreen; + padding: 10px; +} + +.passwordRequestFailure { + border: 1px solid orange; + background-color: 1px solid coral; + padding: 10px; +} + +.cancel { + margin: 10px 0; +} diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js new file mode 100644 index 000000000..1867b9ac2 --- /dev/null +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -0,0 +1,45 @@ +import React from 'react'; + +import {Dialog} from 'coral-ui'; +import Button from 'coral-ui/components/Button'; + +import styles from './BanUserDialog.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => { + const {userName = '', userId = '', commentId = ''} = user; + + return ( + handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}> + handleClose()}>× +
+
+

+ {lang.t('bandialog.ban_user')} +

+
+
+

+ {lang.t('bandialog.are_you_sure', userName)} +

+ + {lang.t('bandialog.note')} + +
+
+ + +
+
+
+ ); +}; + +export default BanUserDialog; diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 3481ad671..380a001c8 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -1,17 +1,20 @@ - import React from 'react'; import timeago from 'timeago.js'; +import Linkify from 'react-linkify'; + import styles from './CommentList.css'; + import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations.json'; -import Linkify from 'react-linkify'; -import {FabButton} from 'coral-ui'; + import {Icon} from 'react-mdl'; +import {FabButton, Button} from 'coral-ui'; const linkify = new Linkify(); // Render a single comment for the list export default props => { + const authorStatus = props.author.get('status'); const {comment, author} = props; const links = linkify.getMatches(comment.get('body')); @@ -28,15 +31,13 @@ export default props => { {links ? Contains Link : null}
- {props.actions.map((action, i) => canShowAction(action, comment) ? ( - props.onClickAction(props.actionsMap[action].status, comment.get('id'))} - /> - ) : null)} + {props.actions.map((action, i) => getActionButton(action, i, props))}
+
+ {authorStatus === 'banned' ? + {lang.t('comment.banned_user')} : null} +
@@ -49,15 +50,33 @@ export default props => { ); }; -// Check if an action can be performed over a comment -const canShowAction = (action, comment) => { - const status = comment.get('status'); - const flagged = comment.get('flagged'); +// Get the button of the action performed over a comment if any +const getActionButton = (action, i, props) => { + const status = props.comment.get('status'); + const flagged = props.comment.get('flagged'); + const banned = (props.author.get('status') === 'banned'); if (action === 'flag' && (status || flagged === true)) { - return false; + return null; } - return true; + if (action === 'ban') { + return ( + + ); + } + return ( + props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))} + /> + ); }; const linkStyles = { diff --git a/client/coral-admin/src/components/CommentList.css b/client/coral-admin/src/components/CommentList.css index 2c58c81cf..fddee7553 100644 --- a/client/coral-admin/src/components/CommentList.css +++ b/client/coral-admin/src/components/CommentList.css @@ -122,7 +122,6 @@ } - .hasLinks { color: #f00; text-align: right; @@ -133,3 +132,14 @@ margin-right: 5px; } } + +.banned { + color: #f00; + text-align: left; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 40b99b892..b4547335e 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -9,7 +9,8 @@ import Comment from 'components/Comment'; const actions = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, - 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'} + 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, + 'ban': {status: 'banned', icon: 'not interested'} }; // Renders a comment list and allow performing actions @@ -19,6 +20,7 @@ export default class CommentList extends React.Component { this.state = {active: null}; this.onClickAction = this.onClickAction.bind(this); + this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this); } // remove key handlers before leaving @@ -99,7 +101,8 @@ export default class CommentList extends React.Component { // If we are performing an action over a comment (aka removing from the list) we need to select a new active. // TODO: In the future this can be improved and look at the actual state to // resolve since the content of the list could change externally. For now it works as expected - onClickAction (action, id) { + onClickAction (action, id, author_id) { + // activate the next comment if (id === this.state.active) { const {commentIds} = this.props; if (commentIds.last() === this.state.active) { @@ -108,7 +111,11 @@ export default class CommentList extends React.Component { this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))}); } } - this.props.onClickAction(action, id); + this.props.onClickAction(action, id, author_id); + } + + onClickShowBanDialog(userId, userName, commentId) { + this.props.onClickShowBanDialog(userId, userName, commentId); } render () { @@ -125,6 +132,7 @@ export default class CommentList extends React.Component { key={index} index={index} onClickAction={this.onClickAction} + onClickShowBanDialog={this.onClickShowBanDialog} actions={this.props.actions} actionsMap={actions} isActive={commentId === active} diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js new file mode 100644 index 000000000..856f619d0 --- /dev/null +++ b/client/coral-admin/src/constants/comments.js @@ -0,0 +1,3 @@ +export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; +export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; +export const USER_BAN_SUCESS = 'USER_BAN_SUCESS'; diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index 2ea77ea77..e3fd88a71 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -4,3 +4,4 @@ export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE'; export const SORT_UPDATE = 'SORT_UPDATE'; export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE'; export const SET_ROLE = 'SET_ROLE'; +export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS'; diff --git a/client/coral-admin/src/containers/Community/Community.css b/client/coral-admin/src/containers/Community/Community.css index 63148da7e..b19d0261d 100644 --- a/client/coral-admin/src/containers/Community/Community.css +++ b/client/coral-admin/src/containers/Community/Community.css @@ -1,7 +1,3 @@ -.dataTable { - width: 100%; -} - .roleButton { display: block; } @@ -9,14 +5,13 @@ .searchInput { display: block; padding-left: 40px; - /*border: none;*/ + width: auto; } .searchBox { - /*border: 1px solid rgba(0,0,0,.12);*/ background: white; } .email { display: block; -} \ No newline at end of file +} diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index f8c24fd3a..e798266f0 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -20,6 +20,10 @@ const tableHeaders = [ title: lang.t('community.account_creation_date'), field: 'created_at' }, + { + title: lang.t('community.status'), + field: 'status' + }, { title: lang.t('community.newsroom_role'), field: 'role' @@ -30,7 +34,7 @@ const Community = ({isFetching, commenters, ...props}) => { const hasResults = !isFetching && !!commenters.length; return ( - +
- + { isFetching && } { !hasResults && } { hasResults && diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index cf67dba4d..e4263cc06 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -40,8 +40,8 @@ class CommunityContainer extends Component { this.props.dispatch(fetchCommenters({ value: this.state.searchValue, - field: community.get('field'), - asc: community.get('asc'), + field: community.field, + asc: community.asc, ...query })); } @@ -66,15 +66,19 @@ class CommunityContainer extends Component { return ( ); } } -export default connect(({community}) => ({community}))(CommunityContainer); +const mapStateToProps = state => ({ + community: state.community.toJS() +}); + +export default connect(mapStateToProps)(CommunityContainer); diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index 89737e33e..1d81b1b2e 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -4,7 +4,7 @@ import {SelectField, Option} from 'react-mdl-selectfield'; import styles from './Community.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations'; -import {setRole} from '../../actions/community'; +import {setRole, setCommenterStatus} from '../../actions/community'; const lang = new I18n(translations); @@ -19,6 +19,10 @@ class Table extends Component { this.props.dispatch(setRole(id, role)); } + onCommenterStatusChange (id, status) { + this.props.dispatch(setCommenterStatus(id, status)); + } + render () { const {headers, commenters, onHeaderClickHandler} = this.props; @@ -46,6 +50,14 @@ class Table extends Component { {row.created_at} + + this.onCommenterStatusChange(row.id, status)}> + + + + this.onCommentAction(action, id)} - actions={['reject', 'approve']} + onClickAction={(action, commentId) => this.onCommentAction(action, commentId)} + onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)} + actions={['reject', 'approve', 'ban']} loading={comments.loading} /> -
+ this.hideBanUserDialog()} + onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)} + user={comments.get('banUser')}/> +
{ case 'COMMENT_FLAG': return flag(state, action); case 'COMMENT_CREATE_SUCCESS': return addComment(state, action); case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state); + case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action); + case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action); + case actions.USER_BAN_SUCESS: return setBanUser(state, false, action); default: return state; } }; +// hide or show the UI for the dialog confirming the ban +// set the user that is going to set and the comment that is the reason +const setBanUser = (state, showBanUser, action) => { + const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId}; + return state.set('showBanUserDialog', showBanUser) + .set('banUser', banUser); +}; + // Update a comment status const updateStatus = (state, action) => { const byId = state.get('byId'); diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 81a09a1cc..367e67b2a 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -5,7 +5,8 @@ import { FETCH_COMMENTERS_FAILURE, FETCH_COMMENTERS_SUCCESS, SORT_UPDATE, - SET_ROLE + SET_ROLE, + SET_COMMENTER_STATUS } from '../constants/community'; const initialState = Map({ @@ -45,6 +46,14 @@ export default function community (state = initialState, action) { commenters[idx].roles[0] = action.role; return state.set('commenters', commenters.map(id => id)); } + case SET_COMMENTER_STATUS: { + const commenters = state.get('commenters'); + const idx = commenters.findIndex(el => el.id === action.id); + + commenters[idx].status = action.status; + return state.set('commenters', commenters.map(id => id)); + + } case SORT_UPDATE : return state .set('field', action.sort.field) diff --git a/client/coral-admin/src/reducers/users.js b/client/coral-admin/src/reducers/users.js index 872ae904a..ef589c155 100644 --- a/client/coral-admin/src/reducers/users.js +++ b/client/coral-admin/src/reducers/users.js @@ -8,6 +8,7 @@ const initialState = Map({ export default (state = initialState, action) => { switch (action.type) { case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state); + case 'USER_STATUS_UPDATE': return updateUserStatus(state, action); default: return state; } }; @@ -18,3 +19,10 @@ const replaceUsers = (action, state) => { return state.set('byId', users) .set('ids', List(users.keys())); }; + +// Update a user status +const updateUserStatus = (state, action) => { + const byId = state.get('byId'); + const data = byId.get(action.author_id).set('status', action.status.toLowerCase()); + return state.set('byId', byId.set(action.author_id, data)); +}; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 5502df7ed..ab542b4a0 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -21,6 +21,9 @@ export default store => next => action => { case 'COMMENT_CREATE': createComment(store, action.name, action.body); break; + case 'USER_BAN': + userStatusUpdate(store, action.status, action.userId, action.commentId); + break; } next(action); @@ -81,3 +84,10 @@ const createComment = (store, name, comment) => { .then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res})) .catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error})); }; + +// Ban a user +const userStatusUpdate = (store, status, userId, commentId) => { + return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) + .then(res => store.dispatch({type: 'USER_BAN_SUCESS', res})) + .catch(error => store.dispatch({type: 'USER_BAN_FAILED', error})); +}; diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 23081d32d..013c159a8 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -6,7 +6,14 @@ "newsroom_role": "Newsroom Role", "admin": "Administrator", "moderator": "Moderator", - "role": "Select role..." + "role": "Select role...", + "no-results": "No users found with that user name or email address.", + "status": "Status", + "select-status": "Select status...", + "active": "Active", + "banned": "Banned", + "banned-user": "Banned User", + "loading": "Loading results" }, "modqueue": { "pending": "pending", @@ -25,7 +32,9 @@ }, "comment": { "flagged": "flagged", - "anon": "Anonymous" + "anon": "Anonymous", + "ban_user": "Ban User", + "banned_user": "Banned User" }, "embedlink": { "copy": "Copy to Clipboard" @@ -38,7 +47,7 @@ "comment-settings": "Comment Settings", "embed-comment-stream": "Embed Comment Stream", "banned-word-header": "Write the bannned words list", - "banned-word-text": "Comments which contain these words or phrases, not seperated by commas and not case sensitive, will be automatically removed from the comment stream.", + "banned-word-text": "Comments which contain these words or phrases, not separated by commas and not case sensitive, will be automatically removed from the comment stream.", "wordlist": "Banned words list", "save-changes": "Save Changes", "copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles", @@ -47,6 +56,13 @@ "community": "Community", "closed-comments-desc": "Write a message for closed threads", "closed-comments-label": "Write a message..." + }, + "bandialog": { + "ban_user": "Ban User?", + "are_you_sure": "Are you sure you would like to ban {0}?", + "note": "Note: Banning this user will also place this comment in the Rejected queue.", + "cancel": "Cancel", + "yes_ban_user": "Yes, Ban User" } }, "es": { @@ -56,7 +72,14 @@ "newsroom_role": "Rol en la redacción", "admin": "Administrador", "moderator": "Moderador", - "role": "Select role..." + "role": "Seleccionar rol...", + "no-results": "No se encontraron usuarios con ese nombre de usuario o correo electronico.", + "status": "Estado", + "select-status": "Seleccionar estado...", + "active": "Activa", + "banned": "Suspendido", + "banned-user": "Usuario Suspendido", + "loading": "Cargando resultados" }, "modqueue": { "pending": "pendiente", @@ -67,7 +90,9 @@ }, "comment": { "flagged": "marcado", - "anon": "Anónimo" + "anon": "Anónimo", + "ban_user": "Suspender Usuario", + "banned_user": "Usuario Suspendido" }, "configure": { "enable-pre-moderation": "Habilitar pre-moderación", @@ -76,9 +101,9 @@ "include-text": "Incluir tu texto aqui.", "comment-settings": "Configuración de Comentarios", "embed-comment-stream": "Colocar Hilo de Comentarios", - "wordlist": "¡traduceme!", - "banned-word-header": "¡traduceme!", - "banned-word-text": "¡traduceme!", + "wordlist": "Lista de palabras no permitidas", + "banned-word-header": "Escribir las palabras no permitidas", + "banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.", "save-changes": "Guardar Cambios", "copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos", "moderate": "Moderar", @@ -86,6 +111,13 @@ "community": "Comunidad", "closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados", "closed-comments-label": "Escribe un mensaje..." + }, + "bandialog": { + "ban_user": "Quieres suspender el Usuario?", + "are_you_sure": "Estas segura que quieres suspender a {props.author.displayName}?", + "note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.", + "cancel": "Cancelar", + "yes_ban_user": "Si, Suspendan el usuario" } } } diff --git a/client/coral-framework/components/CloseCommentsInfo.js b/client/coral-configure/components/CloseCommentsInfo.js similarity index 100% rename from client/coral-framework/components/CloseCommentsInfo.js rename to client/coral-configure/components/CloseCommentsInfo.js diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css new file mode 100644 index 000000000..8526edc09 --- /dev/null +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -0,0 +1,37 @@ +.container { + position: relative; +} + +.apply { + position: absolute; + top: 38%; + transform: translateX(-50%); + right: 0; +} + +ul { + list-style: none; + padding: 0; +} + +ul ul { + padding-left: 20px +} + +.checkbox { + vertical-align: top; + margin: 12px 12px 12px 0; +} + +h4 { + font-size: 14px; + margin-bottom: 5px; +} + +p { + max-width: 380px; +} + +.wrapper { + margin-bottom: 20px; +} diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js new file mode 100644 index 000000000..507c764d7 --- /dev/null +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -0,0 +1,53 @@ +import React from 'react'; +import {Button, Checkbox} from 'coral-ui'; +import styles from './ConfigureCommentStream.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations.json'; +const lang = new I18n(translations); + +export default ({handleChange, handleApply, changed, ...props}) => ( +
+
+

{lang.t('configureCommentStream.title')}

+

{lang.t('configureCommentStream.description')}

+ +
+
    +
  • + +
      +
    • + +
    • +
    +
  • +
+
+); diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js new file mode 100644 index 000000000..d4b71f4e5 --- /dev/null +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -0,0 +1,83 @@ +import React, {Component} from 'react'; +import {connect} from 'react-redux'; + +import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config'; + +import CloseCommentsInfo from '../components/CloseCommentsInfo'; +import ConfigureCommentStream from '../components/ConfigureCommentStream'; + +class ConfigureStreamContainer extends Component { + constructor (props) { + super(props); + + this.state = { + premod: props.config.moderation === 'pre', + premodLinks: false + }; + + this.toggleStatus = this.toggleStatus.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleApply = this.handleApply.bind(this); + } + + handleApply () { + const {premod, changed} = this.state; + const newConfig = { + moderation: premod ? 'pre' : 'post' + }; + if (changed) { + this.props.updateConfiguration(newConfig); + setTimeout(() => { + this.setState({ + changed: false + }); + }, 300); + } + } + + handleChange (e) { + const {name, checked} = e.target; + this.setState({ + [name]: checked, + changed: true + }); + } + + toggleStatus () { + this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open'); + } + + render () { + const {status} = this.props; + return ( +
+ +
+

{status === 'open' ? 'Close' : 'Open'} Comment Stream

+ +
+ ); + } +} + +const mapStateToProps = (state) => ({ + config: state.config.toJS() +}); + +const mapDispatchToProps = dispatch => ({ + updateStatus: status => dispatch(updateOpenStatus(status)), + updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ConfigureStreamContainer); diff --git a/client/coral-configure/translations.json b/client/coral-configure/translations.json new file mode 100644 index 000000000..e597c9c9a --- /dev/null +++ b/client/coral-configure/translations.json @@ -0,0 +1,24 @@ +{ + "en": { + "configureCommentStream": { + "apply": "Apply", + "title": "Configure Comment Stream", + "description": "As an admin you may customize the settings for the comment stream for this article", + "enablePremod": "Enable Premoderation", + "enablePremodDescription": "Moderators must approve any comment before its published.", + "enablePremodLinks": "Pre-Moderate Comments Containing Links", + "enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published." + } + }, + "es": { + "configureCommentStream": { + "apply": "Aplicar", + "title": "Configurar los comentarios", + "description": "Como Administrador puedes modificar las opciones de los comentarios en este artículo", + "enablePremod": "Activar Pre Moderación", + "enablePremodDescription": "Los Moderadores deben aprobar cualquier comentario antes de su publicación", + "enablePremodLinks": "Pre-Moderar Commentarios que contienen Links", + "enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación." + } + } +} diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index f90b96201..7ec042ea1 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -6,8 +6,7 @@ import { itemActions, Notification, notificationActions, - authActions, - configActions + authActions } from '../../coral-framework'; import CommentBox from '../../coral-plugin-commentbox/CommentBox'; @@ -17,7 +16,7 @@ import PubDate from '../../coral-plugin-pubdate/PubDate'; import Count from '../../coral-plugin-comment-count/CommentCount'; import AuthorName from '../../coral-plugin-author-name/AuthorName'; import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'; -import FlagButton from '../../coral-plugin-flags/FlagButton'; +import FlagComment from '../../coral-plugin-flags/FlagComment'; import LikeButton from '../../coral-plugin-likes/LikeButton'; import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton'; import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; @@ -27,12 +26,12 @@ import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui'; import SettingsContainer from '../../coral-settings/containers/SettingsContainer'; import RestrictedContent from '../../coral-framework/components/RestrictedContent'; import SuspendedAccount from '../../coral-framework/components/SuspendedAccount'; -import CloseCommentsInfo from '../../coral-framework/components/CloseCommentsInfo'; + +import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer'; const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions; const {addNotification, clearNotification} = notificationActions; const {logout, showSignInDialog} = authActions; -const {updateOpenStatus} = configActions; class CommentStream extends Component { @@ -44,7 +43,6 @@ class CommentStream extends Component { }; this.changeTab = this.changeTab.bind(this); - this.toggleStatus = this.toggleStatus.bind(this); } changeTab (tab) { @@ -53,10 +51,6 @@ class CommentStream extends Component { }); } - toggleStatus () { - this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open'); - } - static propTypes = { items: PropTypes.object.isRequired, addItem: PropTypes.func.isRequired, @@ -96,9 +90,11 @@ class CommentStream extends Component { const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0]; const rootItem = this.props.items.assets && this.props.items.assets[rootItemId]; const {actions, users, comments} = this.props.items; + const {status, moderation, closedMessage} = this.props.config; const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth; - const {status, closedMessage} = this.props.config; const {activeTab} = this.state; + const banned = (this.props.userData.status === 'banned'); + const expandForLogin = showSignInDialog ? { minHeight: document.body.scrollHeight + 150 } : {}; @@ -112,8 +108,6 @@ class CommentStream extends Component { Configure Stream {loggedIn && } - {/* Add to the restricted param a boolean if the user is suspended*/} - }> { status === 'open' @@ -122,16 +116,20 @@ class CommentStream extends Component { content={this.props.config.infoBoxContent} enable={this.props.config.infoBoxEnable} /> + }> +
:

{closedMessage}

} @@ -141,14 +139,26 @@ class CommentStream extends Component { const comment = comments[commentId]; return

- +
+ currentUser={this.props.auth.user} + showReply={comment.showReply} + banned={banned}/> + currentUser={this.props.auth.user} + banned={banned}/>
- { comment.children && @@ -198,6 +212,8 @@ class CommentStream extends Component { + currentUser={this.props.auth.user} + banned={banned}/>
-
; }) @@ -257,12 +278,13 @@ class CommentStream extends Component { /> -

{status === 'open' ? 'Close' : 'Open'} Comment Stream

- +
- ({ }); const mapDispatchToProps = (dispatch) => ({ - addItem: (item, itemType) => dispatch(addItem(item, itemType)), + addItem: (item, item_id) => dispatch(addItem(item, item_id)), updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)), postItem: (data, type, id) => dispatch(postItem(data, type, id)), getStream: (rootId) => dispatch(getStream(rootId)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), + postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)), showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), - postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)), deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)), appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), handleSignInDialog: () => dispatch(authActions.showSignInDialog()), - logout: () => dispatch(logout()), - updateStatus: status => dispatch(updateOpenStatus(status)) + logout: () => dispatch(logout()) }); export default connect(mapStateToProps, mapDispatchToProps)(CommentStream); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 766ad763a..c27baf406 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -127,6 +127,10 @@ hr { font-weight: bolder; } +.coral-plugin-author-name-bio-flag { + float: right; +} + /* Reply styles */ @@ -197,6 +201,54 @@ hr { margin: 8px; } +/* Flag Styles */ + +.coral-plugin-flags-container { + position: relative; +} + +.coral-plugin-flags-popup span { + min-width: 280px; + bottom: 36px; + left: -190px; + position: absolute; +} + +.coral-plugin-flags-popup-form { + margin-bottom: 10px; +} + +.coral-plugin-flags-popup-header { + font-weight: bolder; + font-size: 16px; + margin-bottom: 10px; +} + +.coral-plugin-flags-popup-radio { + margin:5px; +} + +.coral-plugin-flags-popup-radio-label { + margin:5px; + font-size: 14px; +} + +.coral-plugin-flags-popup-counter { + float: left; + margin-top: 21px; + color: #999; +} + +.coral-plugin-flags-popup-button { + float: right; + margin-top: 10px; +} + +.coral-plugin-flags-other-text { + margin-left: 20px; + width: 75%; +} + /* Close comments */ .close-comments-intro-wrapper { diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 7ad5e4c34..d027dcd9e 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -91,7 +91,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU export const fetchForgotPassword = email => dispatch => { dispatch(forgotPassowordRequest(email)); - coralApi('/user/request-password-reset', {method: 'POST', body: {email}}) + coralApi('/users/request-password-reset', {method: 'POST', body: {email}}) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); }; diff --git a/client/coral-framework/actions/config.js b/client/coral-framework/actions/config.js index e004fa1eb..f85e609eb 100644 --- a/client/coral-framework/actions/config.js +++ b/client/coral-framework/actions/config.js @@ -1,19 +1,33 @@ import coralApi from '../helpers/response'; -/* Config Actions */ +import * as actions from '../constants/config'; +import {addNotification} from '../actions/notification'; -/** - * Action name constants - */ - -export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; -export const OPEN_COMMENTS = 'OPEN_COMMENTS'; -export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; -export const ADD_ITEM = 'ADD_ITEM'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from './../translations'; +const lang = new I18n(translations); export const updateOpenStatus = status => (dispatch, getState) => { const assetId = getState().items.get('assets') .keySeq() .toArray()[0]; return coralApi(`/asset/${assetId}/status?status=${status}`, {method: 'PUT'}) - .then(() => dispatch({type: status === 'open' ? OPEN_COMMENTS : CLOSE_COMMENTS})); + .then(() => dispatch({type: status === 'open' ? actions.OPEN_COMMENTS : actions.CLOSE_COMMENTS})); +}; + +const updateConfigRequest = () => ({type: actions.UPDATE_CONFIG_REQUEST}); +const updateConfigSuccess = config => ({type: actions.UPDATE_CONFIG_SUCCESS, config}); +const updateConfigFailure = () => ({type: actions.UPDATE_CONFIG_FAILURE}); + +export const updateConfiguration = newConfig => (dispatch, getState) => { + const assetId = getState().items.get('assets') + .keySeq() + .toArray()[0]; + + dispatch(updateConfigRequest()); + coralApi(`/asset/${assetId}/settings`, {method: 'PUT', body: newConfig}) + .then(() => { + dispatch(addNotification('success', lang.t('successUpdateSettings'))); + dispatch(updateConfigSuccess(newConfig)); + }) + .catch(error => dispatch(updateConfigFailure(error))); }; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index d7d482991..8f0e789af 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -1,14 +1,9 @@ import coralApi from '../helpers/response'; import {fromJS} from 'immutable'; -/* Item Actions */ - -/** - * Action name constants - */ +import {UPDATE_CONFIG} from '../constants/config'; export const ADD_ITEM = 'ADD_ITEM'; export const UPDATE_ITEM = 'UPDATE_ITEM'; -export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'; /** @@ -106,7 +101,7 @@ export function getStream (assetUrl) { dispatch(addItem(action, 'actions')); }); } else if (type === 'settings') { - dispatch({type: UPDATE_SETTINGS, config: fromJS(json[type])}); + dispatch({type: UPDATE_CONFIG, config: fromJS(json[type])}); } else { json[type].forEach(item => { dispatch(addItem(item, type)); @@ -217,13 +212,8 @@ export function postItem (item, type, id) { * */ -export function postAction (item_id, action_type, user_id, item_type) { +export function postAction (item_id, item_type, action) { return () => { - const action = { - action_type, - user_id - }; - return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action}); }; } diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index 72075e6db..cda2d765d 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -12,7 +12,7 @@ const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error}); export const saveBio = (user_id, formData) => dispatch => { dispatch(saveBioRequest()); - coralApi(`/user/${user_id}/bio`, {method: 'PUT', body: formData}) + coralApi(`/users/${user_id}/bio`, {method: 'PUT', body: formData}) .then(({settings}) => { dispatch(addNotification('success', lang.t('successBioUpdate'))); dispatch(saveBioSuccess(settings)); diff --git a/client/coral-framework/components/SuspendedAccount.js b/client/coral-framework/components/SuspendedAccount.js index 5a23b77a2..46d1f1d58 100644 --- a/client/coral-framework/components/SuspendedAccount.js +++ b/client/coral-framework/components/SuspendedAccount.js @@ -2,7 +2,10 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations.json'; const lang = new I18n(translations); +import styles from './RestrictedContent.css'; export default () => ( - {lang.t('suspendedAccountMsg')} +
+ {lang.t('suspendedAccountMsg')} +
); diff --git a/client/coral-framework/constants/config.js b/client/coral-framework/constants/config.js new file mode 100644 index 000000000..5dca44ba9 --- /dev/null +++ b/client/coral-framework/constants/config.js @@ -0,0 +1,9 @@ +export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST'; +export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS'; +export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE'; + +export const UPDATE_CONFIG = 'UPDATE_CONFIG'; + +export const OPEN_COMMENTS = 'OPEN_COMMENTS'; +export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; +export const ADD_ITEM = 'ADD_ITEM'; diff --git a/client/coral-framework/reducers/config.js b/client/coral-framework/reducers/config.js index 7fbccaa49..8266fbfa1 100644 --- a/client/coral-framework/reducers/config.js +++ b/client/coral-framework/reducers/config.js @@ -1,31 +1,28 @@ -/* @flow */ - import {Map} from 'immutable'; -import * as actions from '../actions/config'; +import * as actions from '../constants/config'; const initialState = Map({ features: Map({}), status: 'open', - closedMessage: '' + moderation: null }); export default (state = initialState, action) => { switch(action.type) { - // Override config if worked - case actions.UPDATE_SETTINGS: - return action.config; - + case actions.UPDATE_CONFIG: + return state + .merge(Map(action.config)); + case actions.UPDATE_CONFIG_SUCCESS: + return state + .merge(Map(action.config)); case actions.OPEN_COMMENTS: - return state.set('status', 'open'); - + return state + .set('status', 'open'); case actions.CLOSE_COMMENTS: - return state.set('status', 'closed'); - + return state + .set('status', 'closed'); case actions.ADD_ITEM: - return action.item_type === 'assets' ? - state.set('status', action.item.status) - : state; - + return action.item_type === 'assets' ? state.set('status', action.item.status) : state; default: return state; } diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index ab65ba565..06a39944a 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -1,5 +1,6 @@ { "en": { + "successUpdateSettings": "The changes you have made have been applied to the comment stream on this article", "successBioUpdate": "Your Bio has been updated", "contentNotAvailable": "This content is not available", "suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information", @@ -13,6 +14,7 @@ } }, "es": { + "successUpdateSettings": "La configuración de este articulo fue actualizada", "successBioUpdate": "Tu bio fue actualizada", "contentNotAvailable": "El contenido no se encuentra disponible", "suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information", diff --git a/client/coral-plugin-author-name/AuthorName.js b/client/coral-plugin-author-name/AuthorName.js index 56b868726..bf970b3c4 100644 --- a/client/coral-plugin-author-name/AuthorName.js +++ b/client/coral-plugin-author-name/AuthorName.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import {Tooltip} from 'coral-ui'; +import FlagBio from '../coral-plugin-flags/FlagBio'; const packagename = 'coral-plugin-author-name'; export default class AuthorName extends Component { @@ -36,7 +37,15 @@ export default class AuthorName extends Component { onMouseLeave={this.handleMouseLeave} > {author && author.displayName} - { showTooltip && {author.settings.bio}} + { showTooltip && +
+ {author.settings.bio} +
+
+ +
+
+ }
); } diff --git a/client/coral-plugin-commentbox/translations.json b/client/coral-plugin-commentbox/translations.json index 2211cbbca..73bef0079 100644 --- a/client/coral-plugin-commentbox/translations.json +++ b/client/coral-plugin-commentbox/translations.json @@ -6,15 +6,15 @@ "name": "Name", "comment-post-notif": "Your comment has been posted.", "comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.", - "comment-post-banned-word": "Your comment contains one or more words which we do not allow. It has been sent to the moderation team for review." + "comment-post-banned-word": "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team." }, "es": { "post": "Publicar", "reply": "Respuesta", "comment": "Comentario", "name": "Nombre", - "comment-post-notif": "¡traduceme!", - "comment-post-notif-premod": "¡traduceme!", - "comment-post-banned-word": "¡traduceme!" + "comment-post-notif": "Tu comentario ha sido publicado.", + "comment-post-notif-premod": "Gracias por comentar. Nuestro equipo de moderación va a revisarlo muy pronto.", + "comment-post-banned-word": "Tu comentario contiene una o más palabras que no estan permitidasen nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación." } } diff --git a/client/coral-plugin-flags/FlagBio.js b/client/coral-plugin-flags/FlagBio.js new file mode 100644 index 000000000..90cc11a7a --- /dev/null +++ b/client/coral-plugin-flags/FlagBio.js @@ -0,0 +1,35 @@ +import React from 'react'; +import FlagButton from './FlagButton'; +import {I18n} from '../coral-framework'; +import translations from './translations.json'; + +const FlagBio = (props) => ; + +const getPopupMenu = [ + () => { + return { + header: lang.t('step-2-header'), + itemType: 'user', + field: 'bio', + options: [ + {val: 'This bio is offensive', text: lang.t('bio-offensive')}, + {val: 'I don\'t like this bio', text: lang.t('no-like-bio')}, + {val: 'This looks like an ad/marketing', text: lang.t('marketing')}, + {val: 'other', text: lang.t('other')} + ], + button: lang.t('continue'), + sets: 'detail' + }; + }, + () => { + return { + header: lang.t('step-3-header'), + text: lang.t('thank-you'), + button: lang.t('done'), + }; + } +]; + +export default FlagBio; + +const lang = new I18n(translations); diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index dd5979888..d0b9cb6b4 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -1,49 +1,178 @@ -import React from 'react'; +import React, {Component} from 'react'; import {I18n} from '../coral-framework'; import translations from './translations.json'; +import {PopupMenu, Button} from 'coral-ui'; +import onClickOutside from 'react-onclickoutside'; const name = 'coral-plugin-flags'; -const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, addNotification, currentUser}) => { - const flagged = flag && flag.current_user; - const onFlagClick = () => { - if (!currentUser) { - const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75; - showSignInDialog(offset); +class FlagButton extends Component { + + state = { + showMenu: false, + showOther: false, + itemType: '', + detail: '', + otherText: '', + step: 0, + posted: false + } + + // When the "report" button is clicked expand the menu + onReportClick = () => { + if (!this.props.currentUser) { + const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75; + this.props.showSignInDialog(offset); return; } - if (!flagged) { - postAction(id, 'flag', currentUser.id, 'comments') + this.setState({showMenu: !this.state.showMenu}); + } + + onPopupContinue = () => { + const {postAction, addItem, updateItem, flag, id, author_id} = this.props; + const {itemType, field, detail, step, otherText, posted} = this.state; + + //Proceed to the next step or close the menu if we've reached the end + if (step + 1 >= this.props.getPopupMenu.length) { + this.setState({showMenu: false}); + } else { + this.setState({step: step + 1}); + } + + // If itemType and detail are both set, post the action + if (itemType && detail && !posted) { + // Set the text from the "other" field if it exists. + const updatedDetail = otherText || detail; + let item_id; + switch(itemType) { + case 'comments': + item_id = id; + break; + case 'user': + item_id = author_id; + break; + } + const action = { + action_type: 'flag', + field, + detail: updatedDetail + }; + postAction(item_id, itemType, action) .then((action) => { let id = `${action.action_type}_${action.item_id}`; addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions'); - updateItem(action.item_id, action.action_type, id, 'comments'); + updateItem(action.item_id, action.action_type, id, action.item_type); + this.setState({posted: true}); }); - addNotification('success', lang.t('flag-notif')); - } else { - deleteAction(flagged.id) - .then(() => { - updateItem(id, 'flag', '', 'comments'); - }); - addNotification('success', lang.t('flag-notif-remove')); } - }; + } - return
- { - flagged - ? {lang.t('flagged')} - : {lang.t('flag')} + this.state.showMenu && +
+ +
{popupMenu.header}
+ { + popupMenu.text && +
{popupMenu.text}
+ } + { + popupMenu.options &&
+ { + popupMenu.options.map((option) => +
+ +
+
+ ) + } + { + this.state.showOther &&
+ +
+
+ } +
+ } +
+ {this.state.step + 1} of {getPopupMenu.length} +
+ { + popupMenu.button && + } +
+
} - flag - -
; -}; + ; + } +} -export default FlagButton; +export default onClickOutside(FlagButton); const styles = { flaggedIcon: { diff --git a/client/coral-plugin-flags/FlagComment.js b/client/coral-plugin-flags/FlagComment.js new file mode 100644 index 000000000..2d5b33039 --- /dev/null +++ b/client/coral-plugin-flags/FlagComment.js @@ -0,0 +1,52 @@ +import React from 'react'; +import FlagButton from './FlagButton'; +import {I18n} from '../coral-framework'; +import translations from './translations.json'; + +const FlagComment = (props) => ; + +const getPopupMenu = [ + () => { + return { + header: lang.t('step-1-header'), + options: [ + {val: 'user', text: lang.t('flag-username')}, + {val: 'comments', text: lang.t('flag-comment')} + ], + button: lang.t('continue'), + sets: 'itemType' + }; + }, + (itemType) => { + const options = itemType === 'comments' ? + [ + {val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')}, + {val: 'This comment is offensive', text: lang.t('comment-offensive')}, + {val: 'This comment reveals personally identifiable infomration', text: lang.t('personal-info')}, + {val: 'other', text: lang.t('other')} + ] + : [ + {val: 'This username is offensive', text: lang.t('username-offensive')}, + {val: 'I don\'t like this username', text: lang.t('no-like-username')}, + {val: 'This looks like an ad/marketing', text: lang.t('marketing')}, + {val: 'other', text: lang.t('other')} + ]; + return { + header: lang.t('step-2-header'), + options, + button: lang.t('continue'), + sets: 'detail' + }; + }, + () => { + return { + header: lang.t('step-3-header'), + text: lang.t('thank-you'), + button: lang.t('done'), + }; + } +]; + +export default FlagComment; + +const lang = new I18n(translations); diff --git a/client/coral-plugin-flags/translations.json b/client/coral-plugin-flags/translations.json index fb2daa883..0c2ce594d 100644 --- a/client/coral-plugin-flags/translations.json +++ b/client/coral-plugin-flags/translations.json @@ -1,14 +1,50 @@ { "en": { - "flag": "Flag", - "flagged": "Flagged", - "flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.", - "flag-notif-remove": "Your flag has been removed." + "report": "Report", + "reported": "Reported", + "report-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.", + "report-notif-remove": "Your report has been removed.", + "step-1-header": "Report an issue", + "step-2-header": "Help us understand", + "step-3-header": "Thank you for your input", + "flag-username": "Flag username", + "flag-comment": "Flag comment", + "continue": "Continue", + "done": "Done", + "no-agree-comment": "I don't agree with this comment", + "comment-offensive": "This comment is offensive", + "personal-info": "This comment reveals personally identifiable information", + "username-offensive": "This username is offensive", + "no-like-username": "I don't like this username", + "bio-offensive": "This bio is offensive", + "no-like-bio": "I don't like this bio", + "marketing": "This looks like an ad/marketing", + "thank-you": "We value your safety and feedback. A moderator will review your flag.", + "flag-reason": "Reason for flag", + "other": "Other" }, "es": { - "flag": "Marcar", - "flagged": "Marcado", - "flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.", - "flag-notif-remove": "¡traduceme!" + "report": "Informe", + "reported": "Informado", + "report-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.", + "report-notif-remove": "Tu marca ha sido eliminada.", + "step-1-header": "Reportar un problema", + "step-2-header": "Ayudanos a entender", + "step-3-header": "Gracias por tu participación", + "flag-username": "Marcar el nombre de usuario", + "flag-comment": "Marcar el comentario", + "continue": "Continuar", + "done": "hecho", + "no-agree-comment": "No estoy de acuerdo con este comentario", + "comment-offensive": "Este comentario es ofensivo", + "personal-info": "Este comentario muestra información personal", + "username-offensive": "Este nombre de usuario es ofensivo", + "no-like-username": "No me gusta ese nombre de usuario", + "bio-offensive": "Esta bio es ofensiva", + "no-like-bio": "No me gusta esta bio", + "marketing": "Esto parece una publicidad/marketing", + "thank-you": "Nos interesa tu protección y comentarios. Un moderador va a mirar tu marca.", + "flag-reason": "Razón por la que marcar", + "other": "Otro" } } diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 0672da071..9e7d06f33 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -4,7 +4,7 @@ import translations from './translations.json'; const name = 'coral-plugin-flags'; -const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser}) => { +const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => { const liked = like && like.current_user; const onLikeClick = () => { if (!currentUser) { @@ -12,8 +12,14 @@ const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDial showSignInDialog(offset); return; } + if (banned) { + return; + } if (!liked) { - postAction(id, 'like', currentUser.id, 'comments') + const action = { + action_type: 'like' + }; + postAction(id, 'comments', action) .then((action) => { let id = `${action.action_type}_${action.item_id}`; addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions'); diff --git a/client/coral-plugin-replies/ReplyButton.js b/client/coral-plugin-replies/ReplyButton.js index 8e39af663..c44012cdb 100644 --- a/client/coral-plugin-replies/ReplyButton.js +++ b/client/coral-plugin-replies/ReplyButton.js @@ -6,7 +6,12 @@ const name = 'coral-plugin-replies'; const ReplyButton = (props) =>