diff --git a/.eslintignore b/.eslintignore index fc0214dd2..fd85e4523 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,5 +26,7 @@ plugins/* !plugins/talk-plugin-toxic-comments !plugins/talk-plugin-remember-sort !plugins/talk-plugin-deep-reply-count +!plugins/talk-plugin-subscriber +!plugins/talk-plugin-flag-details node_modules diff --git a/.gitignore b/.gitignore index bcc7c89b1..9c55f2708 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,7 @@ plugins/* !plugins/talk-plugin-toxic-comments !plugins/talk-plugin-remember-sort !plugins/talk-plugin-deep-reply-count +!plugins/talk-plugin-subscriber +!plugins/talk-plugin-flag-details **/node_modules/* diff --git a/client/coral-admin/src/components/ActionButton.css b/client/coral-admin/src/components/ActionButton.css deleted file mode 100644 index 226006c6b..000000000 --- a/client/coral-admin/src/components/ActionButton.css +++ /dev/null @@ -1,24 +0,0 @@ -.actionButton { - transform: scale(.8); - margin: 0; -} - -.minimal { - width: 45px; - min-width: 0; -} - -.approve__active { - box-shadow: none; - color: white; - background-color: #519954; - cursor: not-allowed; -} - -.reject__active, .rejected__active { - color: white; - background-color: #D03235; - box-shadow: none; - cursor: not-allowed; -} - diff --git a/client/coral-admin/src/components/ActionButton.js b/client/coral-admin/src/components/ActionButton.js deleted file mode 100644 index 77456b939..000000000 --- a/client/coral-admin/src/components/ActionButton.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import styles from './ActionButton.css'; -import {Button} from 'coral-ui'; -import {menuActionsMap} from '../utils/moderationQueueActionsMap'; - -import t from 'coral-framework/services/i18n'; - -const ActionButton = ({type = '', active, ...props}) => { - const typeName = type.toLowerCase(); - let text = menuActionsMap[type].text; - - if (text === 'approve' && active) { - text = 'approved'; - } else if (text === 'reject' && active) { - text = 'rejected'; - } - - return ( - - ); -}; - -ActionButton.propTypes = { - active: PropTypes.bool, - type: PropTypes.oneOf(['APPROVE', 'REJECT']), - minimal: PropTypes.bool, - acceptComment: PropTypes.func, - rejectComment: PropTypes.func, -}; - -export default ActionButton; diff --git a/client/coral-admin/src/components/ApproveButton.css b/client/coral-admin/src/components/ApproveButton.css new file mode 100644 index 000000000..4c42cc233 --- /dev/null +++ b/client/coral-admin/src/components/ApproveButton.css @@ -0,0 +1,44 @@ +.root { + display: block; + color: #519954; + border: solid 2px rgba(81, 153, 84, 0.75); + background: white; + padding: 10px 12px; + box-sizing: border-box; + vertical-align: middle; + line-height: 24px; + font-size: 17px; + height: 47px; + border-radius: 3px; + text-transform: capitalize; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); + width: 129px; + transform: scale(.8); + margin: 0; + + &:hover { + box-shadow: none; + color: white; + background-color: #519954; + cursor: pointer; + } +} + +.active { + box-shadow: none; + color: white; + background-color: #519954; + + &:hover { + cursor: not-allowed; + } +} + +.minimal { + width: 45px; + min-width: 0; +} + +.icon { + margin-right: 5px; +} diff --git a/client/coral-admin/src/components/ApproveButton.js b/client/coral-admin/src/components/ApproveButton.js new file mode 100644 index 000000000..151a5ca29 --- /dev/null +++ b/client/coral-admin/src/components/ApproveButton.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import cn from 'classnames'; +import styles from './ApproveButton.css'; +import {Icon} from 'coral-ui'; + +import t from 'coral-framework/services/i18n'; + +const ApproveButton = ({active, minimal, onClick}) => { + const text = active ? t('modqueue.approved') : t('modqueue.approve'); + return ( + + ); +}; + +ApproveButton.propTypes = { + active: PropTypes.bool, + minimal: PropTypes.bool, + onClick: PropTypes.func, +}; + +export default ApproveButton; + diff --git a/client/coral-admin/src/components/CommentDetails.css b/client/coral-admin/src/components/CommentDetails.css new file mode 100644 index 000000000..1ee1c46dd --- /dev/null +++ b/client/coral-admin/src/components/CommentDetails.css @@ -0,0 +1,18 @@ +.root { + min-height: 25px; + padding: 0 16px; +} + +.moreDetail { + position: absolute; + font-size: 12px; + font-weight: 500; + color: black; + right: 16px; + + &:hover { + opacity: 0.9; + cursor: pointer; + } +} + diff --git a/client/coral-admin/src/components/CommentDetails.js b/client/coral-admin/src/components/CommentDetails.js new file mode 100644 index 000000000..cd504a40f --- /dev/null +++ b/client/coral-admin/src/components/CommentDetails.js @@ -0,0 +1,66 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import styles from './CommentDetails.css'; +import t from 'coral-framework/services/i18n'; +import Slot from 'coral-framework/components/Slot'; +import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty'; + +class CommentDetails extends Component { + state = { + showDetail: false + }; + + constructor () { + super(); + this.state = { + showDetail: false + }; + } + + toggleDetail = () => { + this.setState((state) => ({ + showDetail: !state.showDetail + })); + } + + render() { + const {data, root, comment} = this.props; + const {showDetail} = this.state; + const queryData = { + root, + comment, + }; + + return ( +
+ + + {showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')} + + + + {showDetail && } +
+ ); + } +} + +CommentDetails.propTypes = { + data: PropTypes.object.isRequired, + root: PropTypes.object.isRequired, + comment: PropTypes.object.isRequired, +}; + +export default CommentDetails; diff --git a/client/coral-admin/src/components/FlagBox.css b/client/coral-admin/src/components/FlagBox.css deleted file mode 100644 index 232803203..000000000 --- a/client/coral-admin/src/components/FlagBox.css +++ /dev/null @@ -1,83 +0,0 @@ -.flagBox { - border-top: 1px solid rgba(66, 66, 66, 0.12); - margin-top: 10px; - .container { - padding: 0 14px; - } - - .detail { - padding: 0 20px 16px; - ul { - padding: 0; - list-style: none; - font-size: 12px; - font-weight: 500; - } - } - - .header { - position: relative; - .moreDetail { - float: right; - font-size: 12px; - font-weight: 500; - margin-right: 10px; - margin-top: 8px; - color: black; - - &:hover { - opacity: 0.9; - cursor: pointer; - } - } - i { - vertical-align: middle; - font-size: 12px; - } - ul { - vertical-align: middle; - list-style: none; - display: inline-block; - padding: 0; - margin-left: 10px; - font-size: 12px; - } - } - - h3 { - vertical-align: middle; - margin: 0; - font-weight: 500; - display: inline-block; - margin-left: 7px; - font-size: 12px; - } -} - -.lessDetail { - display: inline-block; - margin-right: 10px; -} - -.subDetail { - font-weight: normal; - color: #888; - - span { - color: black; - } -} - -.username { - color: #393B44; - text-decoration: none; - cursor: pointer; - font-weight: 600; - padding: 2px 5px; - border-radius: 2px; - margin-left: -5px; - transition: background-color 200ms ease; -&:hover { - background-color: #E0E0E0; - } -} diff --git a/client/coral-admin/src/components/FlagBox.js b/client/coral-admin/src/components/FlagBox.js deleted file mode 100644 index 05fd14128..000000000 --- a/client/coral-admin/src/components/FlagBox.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import {Icon} from 'coral-ui'; -import styles from './FlagBox.css'; -import t from 'coral-framework/services/i18n'; - -const shortReasons = { - 'This comment is offensive': t('modqueue.offensive'), - 'This looks like an ad/marketing': t('modqueue.spam_ads'), - 'This user is impersonating': t('modqueue.impersonating'), - 'I don\'t like this username': t('modqueue.dont_like_username'), - 'Other': t('modqueue.other') -}; - -class FlagBox extends Component { - constructor () { - super(); - this.state = { - showDetail: false - }; - } - - toggleDetail = () => { - this.setState((state) => ({ - showDetail: !state.showDetail - })); - } - - reasonMap = (reason) => { - const shortReason = shortReasons[reason]; - - // if the short reason isn't found, just return the long one. - return shortReason ? shortReason : reason; - } - - render() { - const {actionSummaries, actions, viewUserDetail} = this.props; - const {showDetail} = this.state; - - return ( -
-
-
-

{t('community.flags')} ({actionSummaries.length}):

-
    - {actionSummaries.map((action, i) => -
  • {this.reasonMap(action.reason)} ({action.count})
  • - )} -
- {showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')} -
- {showDetail && ( -
-
    - {actionSummaries.map((summary, i) => { - - const actionList = actions.filter((a) => a.reason === summary.reason); - - return ( -
  • - {this.reasonMap(summary.reason)} ({summary.count}) - -
  • - ); - })} -
-
- )} -
-
- ); - } -} - -FlagBox.propTypes = { - actionSummaries: PropTypes.arrayOf(PropTypes.shape({ - reason: PropTypes.string, - count: PropTypes.number - })).isRequired, - actions: PropTypes.arrayOf(PropTypes.shape({ - message: PropTypes.string, - user: PropTypes.shape({username: PropTypes.string}) - })).isRequired -}; - -export default FlagBox; diff --git a/client/coral-admin/src/components/RejectButton.css b/client/coral-admin/src/components/RejectButton.css new file mode 100644 index 000000000..a06cf5f43 --- /dev/null +++ b/client/coral-admin/src/components/RejectButton.css @@ -0,0 +1,44 @@ +.root { + display: block; + color: #D03235; + border: solid 1px #D03235; + background: white; + padding: 10px 11px; + box-sizing: border-box; + vertical-align: middle; + line-height: 24px; + font-size: 17px; + height: 47px; + border-radius: 3px; + text-transform: capitalize; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); + width: 129px; + transform: scale(.8); + margin: 0; + + &:hover { + color: white; + background-color: #D03235; + box-shadow: none; + cursor: pointer; + } +} + +.active { + color: white; + background-color: #D03235; + box-shadow: none; + + &:hover { + cursor: not-allowed; + } +} + +.minimal { + width: 45px; + min-width: 0; +} + +.icon { + margin-right: 5px; +} diff --git a/client/coral-admin/src/components/RejectButton.js b/client/coral-admin/src/components/RejectButton.js new file mode 100644 index 000000000..e01102dc3 --- /dev/null +++ b/client/coral-admin/src/components/RejectButton.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import cn from 'classnames'; +import styles from './RejectButton.css'; +import {Icon} from 'coral-ui'; + +import t from 'coral-framework/services/i18n'; + +const RejectButton = ({active, minimal, onClick}) => { + const text = active ? t('modqueue.rejected') : t('modqueue.reject'); + return ( + + ); +}; + +RejectButton.propTypes = { + active: PropTypes.bool, + minimal: PropTypes.bool, + onClick: PropTypes.func, +}; + +export default RejectButton; + diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index b6b239057..a5955af45 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -5,7 +5,6 @@ import styles from './UserDetail.css'; import {Icon, Button, Drawer, Spinner} from 'coral-ui'; import {Slot} from 'coral-framework/components'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; -import {actionsMap} from '../utils/moderationQueueActionsMap'; import ClickOutside from 'coral-framework/components/ClickOutside'; import LoadMore from '../components/LoadMore'; import cn from 'classnames'; @@ -70,6 +69,7 @@ export default class UserDetail extends React.Component { renderLoaded() { const { + data, root, root: { user, @@ -177,15 +177,15 @@ export default class UserDetail extends React.Component {
{ nodes.map((comment) => { - const status = comment.action_summaries ? 'FLAGGED' : comment.status; const selected = selectedCommentIds.indexOf(comment.id) !== -1; return (this.props.comment.status === 'ACCEPTED' + ? null + : this.props.acceptComment({commentId: this.props.comment.id}) + ); + + reject = () => (this.props.comment.status === 'REJECTED' + ? null + : this.props.rejectComment({commentId: this.props.comment.id}) + ); + render() { const { - actions = [], comment, - viewUserDetail, suspectWords, bannedWords, selected, toggleSelect, className, - user, - ...props + data, } = this.props; - const flagActionSummaries = getActionSummary('FlagActionSummary', comment); - const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction'); - return (
  • -

    +

    {t('comment.view_context')} -

    +
    @@ -88,41 +92,26 @@ class UserDetailComment extends React.Component {
    - {actions.map((action, i) => { - const active = - (action === 'REJECT' && comment.status === 'REJECTED') || - (action === 'APPROVE' && comment.status === 'ACCEPTED'); - return ( - - (comment.status === 'ACCEPTED' - ? null - : props.acceptComment({commentId: comment.id}))} - rejectComment={() => - (comment.status === 'REJECTED' - ? null - : props.rejectComment({commentId: comment.id}))} - /> - ); - })} + +
  • - {flagActions && flagActions.length - ? - : null} + ); } @@ -138,8 +127,9 @@ UserDetailComment.propTypes = { bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired, toggleSelect: PropTypes.func, comment: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, body: PropTypes.string.isRequired, - action_summaries: PropTypes.array, actions: PropTypes.array, created_at: PropTypes.string.isRequired, asset: PropTypes.shape({ diff --git a/client/coral-admin/src/containers/CommentDetails.js b/client/coral-admin/src/containers/CommentDetails.js new file mode 100644 index 000000000..cf8c2c307 --- /dev/null +++ b/client/coral-admin/src/containers/CommentDetails.js @@ -0,0 +1,24 @@ +import {gql} from 'react-apollo'; +import CommentDetails from '../components/CommentDetails'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; +import withFragments from 'coral-framework/hocs/withFragments'; + +const slots = [ + 'adminCommentDetailArea', + 'adminCommentMoreDetails', +]; + +export default withFragments({ + root: gql` + fragment CoralAdmin_CommentDetails_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + } + `, + comment: gql` + fragment CoralAdmin_CommentDetails_comment on Comment { + __typename + ${getSlotFragmentSpreads(slots, 'comment')} + } + ` +})(CommentDetails); diff --git a/client/coral-admin/src/containers/CommentLabels.js b/client/coral-admin/src/containers/CommentLabels.js index af6a0d963..1942d1ad0 100644 --- a/client/coral-admin/src/containers/CommentLabels.js +++ b/client/coral-admin/src/containers/CommentLabels.js @@ -8,6 +8,12 @@ const slots = [ ]; export default withFragments({ + root: gql` + fragment CoralAdmin_CommentLabels_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + } + `, comment: gql` fragment CoralAdmin_CommentLabels_comment on Comment { hasParent diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 5effb2b84..2ae842b1d 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -160,8 +160,10 @@ export const withUserDetailQuery = withQuery(gql` }) { ...CoralAdmin_Moderation_CommentConnection } + ...${getDefinitionName(UserDetailComment.fragments.root)} ${getSlotFragmentSpreads(slots, 'root')} } + ${UserDetailComment.fragments.root} ${commentConnectionFragment} `, { options: ({userId, statuses}) => { diff --git a/client/coral-admin/src/containers/UserDetailComment.js b/client/coral-admin/src/containers/UserDetailComment.js index 9f0199bce..9eef5934f 100644 --- a/client/coral-admin/src/containers/UserDetailComment.js +++ b/client/coral-admin/src/containers/UserDetailComment.js @@ -3,10 +3,20 @@ import UserDetailComment from '../components/UserDetailComment'; import withFragments from 'coral-framework/hocs/withFragments'; import {getDefinitionName} from 'coral-framework/utils'; import CommentLabels from './CommentLabels'; +import CommentDetails from './CommentDetails'; export default withFragments({ + root: gql` + fragment CoralAdmin_UserDetailComment_root on RootQuery { + __typename + ...${getDefinitionName(CommentLabels.fragments.root)} + ...${getDefinitionName(CommentDetails.fragments.root)} + } + ${CommentLabels.fragments.root} + ${CommentDetails.fragments.root} + `, comment: gql` - fragment CoralAdmin_UserDetail_comment on Comment { + fragment CoralAdmin_UserDetailComment_comment on Comment { id body created_at @@ -17,28 +27,13 @@ export default withFragments({ title url } - action_summaries { - count - ... on FlagActionSummary { - reason - } - } - actions { - ... on FlagAction { - id - reason - message - user { - id - username - } - } - } editing { edited } ...${getDefinitionName(CommentLabels.fragments.comment)} + ...${getDefinitionName(CommentDetails.fragments.comment)} } ${CommentLabels.fragments.comment} + ${CommentDetails.fragments.comment} ` })(UserDetailComment); diff --git a/client/coral-admin/src/routes/Community/components/ActionButton.css b/client/coral-admin/src/routes/Community/components/ActionButton.css deleted file mode 100644 index 8f328e8b8..000000000 --- a/client/coral-admin/src/routes/Community/components/ActionButton.css +++ /dev/null @@ -1,4 +0,0 @@ -.actionButton { - transform: scale(.8); - margin: 0; -} diff --git a/client/coral-admin/src/routes/Community/components/ActionButton.js b/client/coral-admin/src/routes/Community/components/ActionButton.js deleted file mode 100644 index 0ef9b8a39..000000000 --- a/client/coral-admin/src/routes/Community/components/ActionButton.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import styles from './ActionButton.css'; -import {Button} from 'coral-ui'; -import {menuActionsMap} from '../../../utils/moderationQueueActionsMap'; - -import t from 'coral-framework/services/i18n'; - -// TODO: Needs refactoring. -const ActionButton = ({type = '', user, ...props}) => { - return ( - - ); -}; - -export default ActionButton; diff --git a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js index 1d78c015a..e617def53 100644 --- a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js @@ -47,7 +47,6 @@ class FlaggedAccounts extends React.Component { this.props.viewUserDetail(this.props.user.id); + showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id}); + approveUser = () => this.props.approveUser({ + userId: this.props.user.id, + }); render() { const { user, - modActionButtons, viewUserDetail, selected, - approveUser, - showRejectUsernameDialog, me, className, } = this.props; @@ -125,14 +126,12 @@ class User extends React.Component {
    - {modActionButtons.map((action, i) => - - )} + +
    diff --git a/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js b/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js index a9877368a..20c411e52 100644 --- a/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js +++ b/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js @@ -51,7 +51,7 @@ class RejectUsernameDialog extends Component { const next = () => this.setState({stage: stage + 1}); const suspend = async () => { try { - await rejectUsername({id: user.user.id, message: this.state.email}); + await rejectUsername({id: user.id, message: this.state.email}); this.props.handleClose(); } catch (err) { diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index 4e60c64d7..6e20a3167 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -78,7 +78,11 @@ const withData = withQuery(gql` ${FlaggedAccounts.fragments.root} ${FlaggedUser.fragments.root} ${FlaggedUser.fragments.me} -`); +`, { + options: { + fetchPolicy: 'network-only', + }, +}); export default compose( connect(mapStateToProps, mapDispatchToProps), diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.css b/client/coral-admin/src/routes/Moderation/components/Comment.css index a1862640f..10cc707fb 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.css +++ b/client/coral-admin/src/routes/Moderation/components/Comment.css @@ -58,6 +58,7 @@ font-size: 14px; line-height: 1.5; font-weight: 300; + margin-bottom: 8px; } .body { diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index f72b0ef12..42c9c2d13 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -3,18 +3,18 @@ import PropTypes from 'prop-types'; import {Link} from 'react-router'; import {Icon} from 'coral-ui'; -import FlagBox from 'coral-admin/src/components/FlagBox'; +import CommentDetails from 'coral-admin/src/components/CommentDetails'; import styles from './Comment.css'; import CommentLabels from 'coral-admin/src/components/CommentLabels'; import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit'; import Slot from 'coral-framework/components/Slot'; -import {getActionSummary} from 'coral-framework/utils'; -import ActionButton from 'coral-admin/src/components/ActionButton'; import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter'; import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; +import ApproveButton from 'coral-admin/src/components/ApproveButton'; +import RejectButton from 'coral-admin/src/components/RejectButton'; import t, {timeago} from 'coral-framework/services/i18n'; @@ -45,11 +45,19 @@ class Comment extends React.Component { return viewUserDetail(comment.user.id); }; + approve = () => (this.props.comment.status === 'ACCEPTED' + ? null + : this.props.acceptComment({commentId: this.props.comment.id}) + ); + + reject = () => (this.props.comment.status === 'REJECTED' + ? null + : this.props.rejectComment({commentId: this.props.comment.id}) + ); + render() { const { - actions = [], comment, - viewUserDetail, suspectWords, bannedWords, selected, @@ -58,15 +66,9 @@ class Comment extends React.Component { root, currentUserId, currentAsset, - acceptComment, - rejectComment, } = this.props; - const flagActionSummaries = getActionSummary('FlagActionSummary', comment); - const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction'); - const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; - const queryData = {root, comment, asset: comment.asset}; return ( @@ -153,28 +155,14 @@ class Comment extends React.Component {
    - {actions.map((action, i) => { - const active = - (action === 'REJECT' && comment.status === 'REJECTED') || - (action === 'APPROVE' && comment.status === 'ACCEPTED'); - return ( - - (comment.status === 'ACCEPTED' - ? null - : acceptComment({commentId: comment.id}))} - rejectComment={() => - (comment.status === 'REJECTED' - ? null - : rejectComment({commentId: comment.id}))} - /> - ); - })} + +
    - - {flagActions && flagActions.length - ? - : null} ); } @@ -214,6 +195,8 @@ Comment.propTypes = { showSuspendUserDialog: PropTypes.func.isRequired, currentUserId: PropTypes.string.isRequired, comment: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, body: PropTypes.string.isRequired, action_summaries: PropTypes.array, actions: PropTypes.array, @@ -230,7 +213,6 @@ Comment.propTypes = { }), data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, - actions: PropTypes.array.isRequired, selected: PropTypes.bool, }; diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 517892a77..47ed56925 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import Comment from '../containers/Comment'; import styles from './ModerationQueue.css'; import EmptyCard from '../../../components/EmptyCard'; -import {actionsMap} from '../../../utils/moderationQueueActionsMap'; import LoadMore from '../../../components/LoadMore'; import ViewMore from './ViewMore'; import t from 'coral-framework/services/i18n'; @@ -140,7 +139,6 @@ class ModerationQueue extends React.Component { if (singleView) { const index = comments.findIndex((comment) => comment.id === selectedCommentId); const comment = comments[index]; - const status = comment.action_summaries ? 'FLAGGED' : comment.status; return (
    { - const status = comment.action_summaries ? 'FLAGGED' : comment.status; return gql` organizationName moderation } + ...${getDefinitionName(Comment.fragments.root)} } + ${Comment.fragments.root} ${commentConnectionFragment} `, { options: (props) => { diff --git a/client/coral-admin/src/utils/moderationQueueActionsMap.js b/client/coral-admin/src/utils/moderationQueueActionsMap.js deleted file mode 100644 index 95b0ecb55..000000000 --- a/client/coral-admin/src/utils/moderationQueueActionsMap.js +++ /dev/null @@ -1,14 +0,0 @@ -export const actionsMap = { - PREMOD: ['APPROVE', 'REJECT'], - FLAGGED: ['APPROVE', 'REJECT'], - REJECTED: ['APPROVE', 'REJECTED'] -}; - -export const menuActionsMap = { - 'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'f'}, - 'REJECTED': {status: 'REJECTED', text: 'rejected', icon: 'close'}, - 'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 'd'}, - 'FLAGGED': {status: 'FLAGGED', text: 'flag', icon: 'flag', filter: 'Untouched'}, - 'BAN': {status: 'BANNED', text: 'ban_user', icon: 'not interested'}, - '': {icon: 'done'} -}; diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 20ed8a1d6..3f6fd4832 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -96,6 +96,11 @@ export default { user { id username + tags { + tag { + name + } + } } action_summaries { count @@ -129,7 +134,19 @@ export default { user: { __typename: 'User', id: auth.user.id, - username: auth.user.username + username: auth.user.username, + tags: tags.map((tag) => ({ + tag: { + name: tag, + created_at: new Date().toISOString(), + __typename: 'Tag' + }, + assigned_by: { + id: auth.user.id, + __typename: 'User' + }, + __typename: 'TagLink' + })), }, created_at: new Date().toISOString(), body, @@ -140,10 +157,6 @@ export default { created_at: new Date().toISOString(), __typename: 'Tag' }, - assigned_by: { - id: auth.user.id, - __typename: 'User' - }, __typename: 'TagLink' })), status: 'NONE', diff --git a/client/coral-framework/components/CommentDetail.css b/client/coral-framework/components/CommentDetail.css new file mode 100644 index 000000000..4806b9522 --- /dev/null +++ b/client/coral-framework/components/CommentDetail.css @@ -0,0 +1,37 @@ +.root { +} + +.headerContainer { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.header { + vertical-align: middle; + margin: 0; + font-weight: 500; + display: inline-block; + font-size: 12px; + line-height: 12px; + margin-right: 7px; +} + +.info { + font-size: 12px; +} + +.details { + padding: 0 20px 16px; + font-size: 12px; + + &:empty { + display: none; + } +} + +.icon { + vertical-align: middle; + font-size: 12px; + margin-right: 7px; +} diff --git a/client/coral-framework/components/CommentDetail.js b/client/coral-framework/components/CommentDetail.js new file mode 100644 index 000000000..2e1a89089 --- /dev/null +++ b/client/coral-framework/components/CommentDetail.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import styles from './CommentDetail.css'; +import {Icon} from 'coral-ui'; + +const CommentDetail = ({icon, header, info, children, className}) => { + return ( +
    +
    + {icon && } +

    {header}:

    +
    {info}
    +
    + {children && +
    + {children} +
    + } +
    + ); +}; + +CommentDetail.propTypes = { + className: PropTypes.string, + header: PropTypes.node, + icon: PropTypes.string, + info: PropTypes.node, + children: PropTypes.node, +}; + +export default CommentDetail; diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index 72dc8c988..51a849448 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -23,8 +23,9 @@ class IfSlotIsEmpty extends React.Component { } isSlotEmpty(props = this.props) { - const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; - return this.context.plugins.isSlotEmpty(slot, reduxState, rest); + const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props; + const slots = Array.isArray(slot) ? slot : [slot]; + return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData)); } render() { @@ -34,7 +35,7 @@ class IfSlotIsEmpty extends React.Component { } IfSlotIsEmpty.propTypes = { - slot: PropTypes.string, + slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 46e68fe60..6186e2166 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -23,8 +23,9 @@ class IfSlotIsNotEmpty extends React.Component { } isSlotEmpty(props = this.props) { - const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; - return this.context.plugins.isSlotEmpty(slot, reduxState, rest); + const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props; + const slots = Array.isArray(slot) ? slot : [slot]; + return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData)); } render() { @@ -34,7 +35,7 @@ class IfSlotIsNotEmpty extends React.Component { } IfSlotIsNotEmpty.propTypes = { - slot: PropTypes.string, + slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/hocs/excludeIf.js b/client/coral-framework/hocs/excludeIf.js index ec2f0f27a..524c97eb3 100644 --- a/client/coral-framework/hocs/excludeIf.js +++ b/client/coral-framework/hocs/excludeIf.js @@ -1,4 +1,19 @@ -export default (condition) => (BaseComponent) => { - BaseComponent.isExcluded = condition; - return BaseComponent; -}; +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; + +/** + * ExcludeIf provides a property `emit: (eventName, value)` + * to the wrapped component. + */ +export default (condition) => hoistStatics((WrappedComponent) => { + class ExcludeIf extends React.Component { + render() { + return ; + } + } + + WrappedComponent.isExcluded = condition; + return ExcludeIf; +}); diff --git a/client/coral-ui/components/Button.css b/client/coral-ui/components/Button.css index 1b7efb205..ce2cff350 100644 --- a/client/coral-ui/components/Button.css +++ b/client/coral-ui/components/Button.css @@ -130,59 +130,6 @@ background: #00a291; } -.type--approve { - display: block; - color: #519954; - border: solid 2px rgba(81, 153, 84, 0.75); - background: white; - padding: 10px 12px; - box-sizing: border-box; - vertical-align: middle; - line-height: 24px; - font-size: 17px; - height: 47px; - border-radius: 3px; - text-transform: capitalize; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); - width: 129px; - - &:hover { - box-shadow: none; - color: white; - background-color: #519954; - } -} - -.type--reject, .type--rejected { - display: block; - color: #D03235; - border: solid 1px #D03235; - background: white; - padding: 10px 11px; - box-sizing: border-box; - vertical-align: middle; - line-height: 24px; - font-size: 17px; - height: 47px; - border-radius: 3px; - text-transform: capitalize; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); - width: 129px; - - &:hover { - color: white; - background-color: #D03235; - box-shadow: none; - } -} - -.type--rejected { - color: white; - background-color: #D03235; - box-shadow: none; - cursor: not-allowed; -} - .type--ban, .type--actions { display: block; color: #616161; diff --git a/locales/en.yml b/locales/en.yml index 572f810ee..a979739b5 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -262,11 +262,9 @@ en: ban_user: "Ban" billion: B close: Close - dont_like_username: "Don't like username" empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️" flagged: flagged reported: reported - impersonating: Impersonating less_detail: "Less detail" likes: likes million: M @@ -277,9 +275,7 @@ en: newest_first: "Newest First" navigation: Navigation next_comment: "Go to the next comment" - offensive: Offensive oldest_first: "Oldest First" - other: Other premod: pre-mod prev_comment: "Go to the previous comment" reject: "Reject" @@ -290,7 +286,6 @@ en: shortcuts: "Shortcuts" show_shortcuts: "Show Shortcuts" singleview: "Toggle single comment edit view" - spam_ads: Spam/Ads thismenu: "Open this menu" thousand: k try_these: "Try these" diff --git a/locales/es.yml b/locales/es.yml index 4cb630273..e9844667d 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -255,10 +255,8 @@ es: approved: "Aprobado" billion: B close: Cerrar - dont_like_username: "No me gusta el nombre de usuario" empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️" flagged: Reportado - impersonating: Impersonando less_detail: "Menos detalles" likes: likes million: M @@ -269,9 +267,7 @@ es: newest_first: "Primero el más nuevo" navigation: Navegación next_comment: "Ir al siguiente comentario" - offensive: Ofensivo oldest_first: "Primero el más antiguo" - other: Otro premod: pre-mod prev_comment: "Ir al comentario anterior" reject: "Rechazar" @@ -282,7 +278,6 @@ es: shortcuts: Atajos show_shortcuts: "Mostrar Atajos" singleview: "Colocar vista de edición de comentario único" - spam_ads: Spam/Publicidad thismenu: "Abrir este menu" thousand: k try_these: "Intentar estos" diff --git a/locales/fr.yml b/locales/fr.yml index 0638a8896..f8ff403e7 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -41,22 +41,17 @@ fr: banned: Banni banned_user: "Utilisateur banni" cancel: Signalé - dont_like_username: "Je n'aime pas ce nom d'utilisateur" flaggedaccounts: "Noms d'utilisateurs signalés" flags: Signalements - impersonating: "Cet utilisateur se fait passer pour quelqu'un d'autre" loading: "Chargement des résultats" moderator: Modérateur newsroom_role: "Rôle de la salle de presse" no_flagged_accounts: "La liste des comptes signalés est vide." no_results: "Aucun utilisateur n'a été trouvé avec ce nom d'utilisateur ou cette adresse de messagerie. Ils se cachent !" note: "Remarque: bannir cet utilisateur ne lui permettra pas de modifier les commentaires ou de supprimer quoi que ce soit." - offensive: "Ce commentaire est offensant" - other: Autre people: Gens role: "Sélectionnez le rôle ..." select_status: "Sélectionnez l'état ..." - spam_ads: "Spam / Annonces" staff: "Équipe" status: Statut username_and_email: "Nom d'utilisateur et e-mail" diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index 4cf1a9000..3f2c92628 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -51,7 +51,6 @@ pt_BR: banned: Proibida banned_user: "Usuário proibido" cancel: Cancelar - dont_like_username: "Não gostei do nome de usuário" flaggedaccounts: "Nomes de usuários marcados" flags: Marcadas impersonating: "Representação" @@ -60,12 +59,9 @@ pt_BR: newsroom_role: "Papel de empresa" no_flagged_accounts: "A fila de nomes de usuários marcados está atualmente vazia." no_results: "Nenhum usuário encontrado com esse nome de usuário ou e-mail. Eles estão se escondendo!" - offensive: "Ofensiva" - other: Outra people: Pessoas role: "Selecione um papel..." select_status: "Selecione um status..." - spam_ads: "Spam/anúncios" staff: "Funcionários" status: Status username_and_email: "Nome de usuário e email" diff --git a/plugin-api/beta/client/actions/admin.js b/plugin-api/beta/client/actions/admin.js new file mode 100644 index 000000000..2a32e57bf --- /dev/null +++ b/plugin-api/beta/client/actions/admin.js @@ -0,0 +1 @@ +export {viewUserDetail} from 'coral-admin/src/actions/userDetail'; diff --git a/plugin-api/beta/client/components/index.js b/plugin-api/beta/client/components/index.js index 392d923a0..17e8e7a13 100644 --- a/plugin-api/beta/client/components/index.js +++ b/plugin-api/beta/client/components/index.js @@ -4,4 +4,5 @@ export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty'; export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName'; export {default as CommentTimestamp} from 'coral-framework/components/CommentTimestamp'; +export {default as CommentDetail} from 'coral-framework/components/CommentDetail'; export {default as CommentContent} from 'coral-framework/components/CommentContent'; diff --git a/plugins.default.json b/plugins.default.json index 6ce126983..a26a38c8e 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -21,6 +21,7 @@ "talk-plugin-author-menu", "talk-plugin-member-since", "talk-plugin-ignore-user", - "talk-plugin-moderation-actions" + "talk-plugin-moderation-actions", + "talk-plugin-flag-details" ] } diff --git a/plugins/talk-plugin-flag-details/.eslintrc.json b/plugins/talk-plugin-flag-details/.eslintrc.json new file mode 100644 index 000000000..78f7c2397 --- /dev/null +++ b/plugins/talk-plugin-flag-details/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk" +} diff --git a/plugins/talk-plugin-flag-details/client/.babelrc b/plugins/talk-plugin-flag-details/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-flag-details/client/.eslintrc.json b/plugins/talk-plugin-flag-details/client/.eslintrc.json new file mode 100644 index 000000000..c8a6db18a --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk/client" +} diff --git a/plugins/talk-plugin-flag-details/client/components/FlagDetails.css b/plugins/talk-plugin-flag-details/client/components/FlagDetails.css new file mode 100644 index 000000000..3b59653c2 --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/components/FlagDetails.css @@ -0,0 +1,45 @@ + +.info { + vertical-align: middle; + list-style: none; + display: inline-block; + padding: 0; + font-size: 12px; + margin: 0; +} + +.detail { + margin: 0; + padding: 0; + list-style: none; + font-size: 12px; + font-weight: 500; +} + +.subDetail { + margin-left:10px; + padding: 0; + list-style: none; + font-size: 12px; + font-weight: normal; + color: #888; +} + +.lessDetail { + display: inline-block; + margin-right: 10px; +} + +.username { + color: #393B44; + text-decoration: none; + cursor: pointer; + font-weight: 600; + padding: 2px 5px; + border-radius: 2px; + margin-left: -5px; + transition: background-color 200ms ease; + &:hover { + background-color: #E0E0E0; + } +} diff --git a/plugins/talk-plugin-flag-details/client/components/FlagDetails.js b/plugins/talk-plugin-flag-details/client/components/FlagDetails.js new file mode 100644 index 000000000..9e1d03c72 --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/components/FlagDetails.js @@ -0,0 +1,72 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import styles from './FlagDetails.css'; +import {t} from 'plugin-api/beta/client/services'; +import {Slot, IfSlotIsNotEmpty, CommentDetail} from 'plugin-api/beta/client/components'; + +class FlagDetails extends Component { + + render() { + const {comment: {actions}, more, data, root, comment} = this.props; + + const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction'); + const summaries = flagActions.reduce((sum, action) => { + if (!(action.reason in sum)) { + sum[action.reason] = {count: 0, actions: []}; + } + sum[action.reason].count++; + if (action.user) { + sum[action.reason].userFlagged = true; + } + return sum; + }, {}); + + const reasons = Object.keys(summaries); + const queryData = { + root, + comment, + }; + + return ( + + {reasons.map((reason) => +
  • + {reason} {summaries[reason].userFlagged && `(${summaries[reason].count})`} +
  • + )} + + }> + {more && ( + + + + )} +
    + ); + } +} + +FlagDetails.propTypes = { + more: PropTypes.bool, + data: PropTypes.object, + root: PropTypes.object, + comment: PropTypes.shape({ + actions: PropTypes.arrayOf(PropTypes.shape({ + message: PropTypes.string, + user: PropTypes.shape({username: PropTypes.string}) + })).isRequired, + }).isRequired, +}; + +export default FlagDetails; diff --git a/plugins/talk-plugin-flag-details/client/components/UserFlagDetails.css b/plugins/talk-plugin-flag-details/client/components/UserFlagDetails.css new file mode 100644 index 000000000..3b59653c2 --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/components/UserFlagDetails.css @@ -0,0 +1,45 @@ + +.info { + vertical-align: middle; + list-style: none; + display: inline-block; + padding: 0; + font-size: 12px; + margin: 0; +} + +.detail { + margin: 0; + padding: 0; + list-style: none; + font-size: 12px; + font-weight: 500; +} + +.subDetail { + margin-left:10px; + padding: 0; + list-style: none; + font-size: 12px; + font-weight: normal; + color: #888; +} + +.lessDetail { + display: inline-block; + margin-right: 10px; +} + +.username { + color: #393B44; + text-decoration: none; + cursor: pointer; + font-weight: 600; + padding: 2px 5px; + border-radius: 2px; + margin-left: -5px; + transition: background-color 200ms ease; + &:hover { + background-color: #E0E0E0; + } +} diff --git a/plugins/talk-plugin-flag-details/client/components/UserFlagDetails.js b/plugins/talk-plugin-flag-details/client/components/UserFlagDetails.js new file mode 100644 index 000000000..4b1b173ec --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/components/UserFlagDetails.js @@ -0,0 +1,59 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import styles from './UserFlagDetails.css'; + +class UserFlagDetails extends Component { + + render() { + const {comment: {actions}, viewUserDetail} = this.props; + + const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction'); + const summaries = flagActions.reduce((sum, action) => { + if (!action.user) { + return sum; + } + if (!(action.reason in sum)) { + sum[action.reason] = {count: 0, actions: []}; + } + sum[action.reason].count++; + sum[action.reason].actions.push(action); + return sum; + }, {}); + + return ( + + ); + } +} + +UserFlagDetails.propTypes = { + comment: PropTypes.shape({ + actions: PropTypes.arrayOf(PropTypes.shape({ + message: PropTypes.string, + user: PropTypes.shape({username: PropTypes.string}) + })).isRequired, + }).isRequired, + viewUserDetail: PropTypes.func.isRequired, +}; + +export default UserFlagDetails; diff --git a/plugins/talk-plugin-flag-details/client/containers/FlagDetails.js b/plugins/talk-plugin-flag-details/client/containers/FlagDetails.js new file mode 100644 index 000000000..da04dd65f --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/containers/FlagDetails.js @@ -0,0 +1,39 @@ +import {compose, gql} from 'react-apollo'; +import FlagDetails from '../components/FlagDetails'; +import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; +import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils'; + +const slots = [ + 'adminCommentMoreFlagDetails', +]; + +const enhance = compose( + withFragments({ + root: gql` + fragment CoralAdmin_FlagDetails_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + } + `, + comment: gql` + fragment CoralAdmin_FlagDetails_comment on Comment { + actions { + __typename + ... on FlagAction { + id + reason + message + user { + id + username + } + } + } + ${getSlotFragmentSpreads(slots, 'comment')} + } + ` + }), + excludeIf(({comment: {actions}}) => !actions.some((action) => action.__typename === 'FlagAction')), +); + +export default enhance(FlagDetails); diff --git a/plugins/talk-plugin-flag-details/client/containers/UserFlagDetails.js b/plugins/talk-plugin-flag-details/client/containers/UserFlagDetails.js new file mode 100644 index 000000000..aedbb1de5 --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/containers/UserFlagDetails.js @@ -0,0 +1,39 @@ +import {compose, gql} from 'react-apollo'; +import UserFlagDetails from '../components/UserFlagDetails'; +import {bindActionCreators} from 'redux'; +import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; +import {viewUserDetail} from 'plugin-api/beta/client/actions/admin'; +import {connect} from 'react-redux'; + +const mapDispatchToProps = (dispatch) => ({ + ...bindActionCreators({ + viewUserDetail, + }, dispatch) +}); + +const enhance = compose( + connect(null, mapDispatchToProps), + withFragments({ + comment: gql` + fragment CoralAdmin_UserFlagDetails_comment on Comment { + actions { + __typename + ... on FlagAction { + id + reason + message + user { + id + username + } + } + } + } + ` + }), + excludeIf(({comment: {actions}}) => + !actions.some((action) => action.__typename === 'FlagAction' && action.user + )), +); + +export default enhance(UserFlagDetails); diff --git a/plugins/talk-plugin-flag-details/client/index.js b/plugins/talk-plugin-flag-details/client/index.js new file mode 100644 index 000000000..95f579da5 --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/index.js @@ -0,0 +1,11 @@ +import FlagDetails from './containers/FlagDetails'; +import UserFlagDetails from './containers/UserFlagDetails'; +import translations from './translations.yml'; + +export default { + translations, + slots: { + adminCommentDetailArea: [FlagDetails], + adminCommentMoreFlagDetails: [UserFlagDetails], + } +}; diff --git a/plugins/talk-plugin-flag-details/client/translations.yml b/plugins/talk-plugin-flag-details/client/translations.yml new file mode 100644 index 000000000..7f4e8d1b7 --- /dev/null +++ b/plugins/talk-plugin-flag-details/client/translations.yml @@ -0,0 +1,6 @@ +en: + talk-plugin-flag-details: + flags: Flags +es: + talk-plugin-flag-details: + flags: Reportes diff --git a/plugins/talk-plugin-flag-details/index.js b/plugins/talk-plugin-flag-details/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-flag-details/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/plugins/talk-plugin-subscriber/.eslintrc.json b/plugins/talk-plugin-subscriber/.eslintrc.json new file mode 100644 index 000000000..d7d291614 --- /dev/null +++ b/plugins/talk-plugin-subscriber/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": "@coralproject/eslint-config-talk" + } + \ No newline at end of file diff --git a/plugins/talk-plugin-subscriber/client/.babelrc b/plugins/talk-plugin-subscriber/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-subscriber/client/.eslintrc.json b/plugins/talk-plugin-subscriber/client/.eslintrc.json new file mode 100644 index 000000000..c8a6db18a --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk/client" +} diff --git a/plugins/talk-plugin-subscriber/client/components/SubscriberBadge.css b/plugins/talk-plugin-subscriber/client/components/SubscriberBadge.css new file mode 100644 index 000000000..42edfd8ff --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/components/SubscriberBadge.css @@ -0,0 +1,12 @@ +.badge { + background-color: #616161; + color: white; + display: inline-block; + margin: 0px 5px; + padding: 5px 5px; + border-radius: 2px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.6px; + font-weight: bold; +} \ No newline at end of file diff --git a/plugins/talk-plugin-subscriber/client/components/SubscriberBadge.js b/plugins/talk-plugin-subscriber/client/components/SubscriberBadge.js new file mode 100644 index 000000000..60fe19dc9 --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/components/SubscriberBadge.js @@ -0,0 +1,9 @@ +import React from 'react'; +import styles from './SubscriberBadge.css'; +import {t} from 'plugin-api/beta/client/services'; + +export default () =>( + + {t('talk-plugin-subscriber.subscriber')} + +); diff --git a/plugins/talk-plugin-subscriber/client/containers/SubscriberBadge.js b/plugins/talk-plugin-subscriber/client/containers/SubscriberBadge.js new file mode 100644 index 000000000..d00e5fc05 --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/containers/SubscriberBadge.js @@ -0,0 +1,23 @@ +import SubscriberBadge from '../components/SubscriberBadge'; +import {compose, gql} from 'react-apollo'; +import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; + +const isSubscriber = (tags = []) => tags.some((t) => t.tag.name === 'SUBSCRIBER'); + +const enhance = compose( + withFragments({ + comment: gql` + fragment TalkSubscriberBadge_SubscriberBadge_comment on Comment { + user { + tags { + tag { + name + } + } + } + }` + }), + excludeIf(({comment}) => !isSubscriber(comment.user.tags)) +); + +export default enhance(SubscriberBadge); diff --git a/plugins/talk-plugin-subscriber/client/index.js b/plugins/talk-plugin-subscriber/client/index.js new file mode 100644 index 000000000..937191d41 --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/index.js @@ -0,0 +1,9 @@ +import SubscriberBadge from './containers/SubscriberBadge'; +import translations from './translations.yml'; + +export default { + translations, + slots: { + commentAuthorTags: [SubscriberBadge] + } +}; diff --git a/plugins/talk-plugin-subscriber/client/translations.yml b/plugins/talk-plugin-subscriber/client/translations.yml new file mode 100644 index 000000000..851f818b7 --- /dev/null +++ b/plugins/talk-plugin-subscriber/client/translations.yml @@ -0,0 +1,6 @@ +en: + talk-plugin-subscriber: + subscriber: "Subscriber" +es: + talk-plugin-subscriber: + subscriber: "Subscriptor" diff --git a/plugins/talk-plugin-subscriber/index.js b/plugins/talk-plugin-subscriber/index.js new file mode 100644 index 000000000..ad47ef264 --- /dev/null +++ b/plugins/talk-plugin-subscriber/index.js @@ -0,0 +1,14 @@ +module.exports = { + tags: [ + { + name: 'SUBSCRIBER', + permissions: { + public: true, + self: false, + roles: [] + }, + models: ['USERS'], + created_at: new Date() + } + ] +}; diff --git a/plugins/talk-plugin-subscriber/package.json b/plugins/talk-plugin-subscriber/package.json new file mode 100644 index 000000000..4725abb9e --- /dev/null +++ b/plugins/talk-plugin-subscriber/package.json @@ -0,0 +1,11 @@ +{ + "name": "@coralproject/talk-plugin-subscriber", + "version": "0.1.0", + "description": "A Recipe to display a user badge", + "main": "index.js", + "author": "The Coral Project Team ", + "license": "Apache-2.0", + "dependencies": { + "moment": "^2.18.1" + } +} \ No newline at end of file diff --git a/plugins/talk-plugin-subscriber/yarn.lock b/plugins/talk-plugin-subscriber/yarn.lock new file mode 100644 index 000000000..60fdd97df --- /dev/null +++ b/plugins/talk-plugin-subscriber/yarn.lock @@ -0,0 +1,11 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +moment@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +momentjs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/momentjs/-/momentjs-2.0.0.tgz#73df904b4fa418f6e3c605e831cef6ed5518ebd4" diff --git a/plugins/talk-plugin-toxic-comments/client/components/ToxicDetail.css b/plugins/talk-plugin-toxic-comments/client/components/ToxicDetail.css new file mode 100644 index 000000000..cbf7ce8de --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/components/ToxicDetail.css @@ -0,0 +1,11 @@ +.info { + background-color: rgba(44, 44, 44, 0.89); + color: white; + padding: 2px 4px; + font-size: 0.8em; + margin-left: 6px; +} + +.toxic { + background-color: #d03235; +} diff --git a/plugins/talk-plugin-toxic-comments/client/components/ToxicDetail.js b/plugins/talk-plugin-toxic-comments/client/components/ToxicDetail.js new file mode 100644 index 000000000..1c06c7dfa --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/components/ToxicDetail.js @@ -0,0 +1,47 @@ +import React from 'react'; +import {CommentDetail} from 'plugin-api/beta/client/components'; +import {isToxic} from '../utils'; +import styles from './ToxicDetail.css'; +import cn from 'classnames'; +import PropTypes from 'prop-types'; +import {t} from 'plugin-api/beta/client/services'; + +const getInfo = (toxicity, actions) => { + const toxic = isToxic(actions); + let text = t('talk-plugin-toxic-comments.unlikely'); + if (toxicity > 0.8) { + text = t('talk-plugin-toxic-comments.highly_likely'); + } + else if (toxicity >= 0.5) { + text = t('talk-plugin-toxic-comments.possibly'); + } + else if (toxicity >= 0.7) { + text = t('talk-plugin-toxic-comments.likely'); + } + + return ( +
    + {text} + + {Math.round(toxicity * 100)}% + +
    + ); +}; + +const ToxicLabel = ({comment: {actions, toxicity}}) => ( + +); + +ToxicLabel.propTypes = { + comment: PropTypes.shape({ + actions: PropTypes.array, + toxicity: PropTypes.toxicity, + }), +}; + +export default ToxicLabel; diff --git a/plugins/talk-plugin-toxic-comments/client/containers/ToxicCommentDetail.js b/plugins/talk-plugin-toxic-comments/client/containers/ToxicCommentDetail.js new file mode 100644 index 000000000..5c98fc29d --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/containers/ToxicCommentDetail.js @@ -0,0 +1,10 @@ +import {compose} from 'react-apollo'; +import {excludeIf} from 'plugin-api/beta/client/hocs'; +import ToxicDetail from './ToxicDetail'; +import {isToxic} from '../utils'; + +const enhance = compose( + excludeIf(({comment: {toxicity, actions}}) => toxicity === null || isToxic(actions)), +); + +export default enhance(ToxicDetail); diff --git a/plugins/talk-plugin-toxic-comments/client/containers/ToxicCommentFlagDetail.js b/plugins/talk-plugin-toxic-comments/client/containers/ToxicCommentFlagDetail.js new file mode 100644 index 000000000..a43501ea4 --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/containers/ToxicCommentFlagDetail.js @@ -0,0 +1,10 @@ +import {compose} from 'react-apollo'; +import {excludeIf} from 'plugin-api/beta/client/hocs'; +import ToxicDetail from './ToxicDetail'; +import {isToxic} from '../utils'; + +const enhance = compose( + excludeIf(({comment: {toxicity, actions}}) => toxicity === null || !isToxic(actions)), +); + +export default enhance(ToxicDetail); diff --git a/plugins/talk-plugin-toxic-comments/client/containers/ToxicDetail.js b/plugins/talk-plugin-toxic-comments/client/containers/ToxicDetail.js new file mode 100644 index 000000000..78681261a --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/containers/ToxicDetail.js @@ -0,0 +1,20 @@ +import {compose, gql} from 'react-apollo'; +import {withFragments} from 'plugin-api/beta/client/hocs'; +import ToxicDetail from '../components/ToxicDetail'; + +const enhance = compose( + withFragments({ + comment: gql` + fragment TalkToxicComments_ToxicDetail_Comment on Comment { + toxicity + actions { + __typename + ... on FlagAction { + reason + } + } + }`, + }), +); + +export default enhance(ToxicDetail); diff --git a/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js b/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js index 0d2e9e14b..ee43f4a7e 100644 --- a/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js +++ b/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js @@ -1,15 +1,12 @@ import {compose, gql} from 'react-apollo'; import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; import ToxicLabel from '../components/ToxicLabel'; - -function isToxic(actions) { - return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TOXIC_COMMENT'); -} +import {isToxic} from '../utils'; const enhance = compose( withFragments({ comment: gql` - fragment TalkToxicComments_Comment on Comment { + fragment TalkToxicComments_ToxicLabel_Comment on Comment { actions { __typename ... on FlagAction { diff --git a/plugins/talk-plugin-toxic-comments/client/index.js b/plugins/talk-plugin-toxic-comments/client/index.js index 8a16b8dea..742e002d1 100644 --- a/plugins/talk-plugin-toxic-comments/client/index.js +++ b/plugins/talk-plugin-toxic-comments/client/index.js @@ -1,11 +1,15 @@ import translations from './translations.yml'; import CheckToxicityHook from './containers/CheckToxicityHook'; import ToxicLabel from './containers/ToxicLabel'; +import ToxicCommentDetail from './containers/ToxicCommentDetail'; +import ToxicCommentFlagDetail from './containers/ToxicCommentFlagDetail'; export default { translations, slots: { commentInputDetailArea: [CheckToxicityHook], adminCommentLabels: [ToxicLabel], + adminCommentMoreDetails: [ToxicCommentDetail], + adminCommentMoreFlagDetails: [ToxicCommentFlagDetail], }, }; diff --git a/plugins/talk-plugin-toxic-comments/client/translations.yml b/plugins/talk-plugin-toxic-comments/client/translations.yml index 787095046..95e546e50 100644 --- a/plugins/talk-plugin-toxic-comments/client/translations.yml +++ b/plugins/talk-plugin-toxic-comments/client/translations.yml @@ -4,6 +4,11 @@ en: Are you sure? The language in this comment might violate our community guidelines. You can edit the comment or submit it for moderator review. talk-plugin-toxic-comments: + unlikely: Unlikely + highly_likely: Highly Likely + possibly: Possibly + likely: Likely + toxic_comment: Toxic Comment still_toxic: | This edited comment might still violate our community guidelines. Our moderation team will review your comment shortly. diff --git a/plugins/talk-plugin-toxic-comments/client/utils.js b/plugins/talk-plugin-toxic-comments/client/utils.js new file mode 100644 index 000000000..57bfd9633 --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/utils.js @@ -0,0 +1,4 @@ +export function isToxic(actions) { + return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TOXIC_COMMENT'); +} + diff --git a/plugins/talk-plugin-toxic-comments/index.js b/plugins/talk-plugin-toxic-comments/index.js index 0802a6c11..444cb94f5 100644 --- a/plugins/talk-plugin-toxic-comments/index.js +++ b/plugins/talk-plugin-toxic-comments/index.js @@ -1,8 +1,10 @@ const {readFileSync} = require('fs'); const path = require('path'); const hooks = require('./server/hooks'); +const resolvers = require('./server/resolvers'); module.exports = { typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), hooks, + resolvers, }; diff --git a/plugins/talk-plugin-toxic-comments/server/resolvers.js b/plugins/talk-plugin-toxic-comments/server/resolvers.js new file mode 100644 index 000000000..5af584b44 --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/resolvers.js @@ -0,0 +1,7 @@ +const get = require('lodash/get'); + +module.exports = { + Comment: { + toxicity: (comment) => get(comment, 'metadata.perspective.SEVERE_TOXICITY.summaryScore'), + } +}; diff --git a/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql b/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql index b4dfc8345..c9e28fb9c 100644 --- a/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql +++ b/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql @@ -5,3 +5,6 @@ input CreateCommentInput { checkToxicity: Boolean } +type Comment { + toxicity: Float +}