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 (
+
+ );
+ }
+}
+
+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 (
-
-
-
- {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 (
+
+ {Object.keys(summaries)
+ .map((reason) => (
+ -
+ {reason} ({summaries[reason].count})
+
+
+ ))
+ }
+
+ );
+ }
+}
+
+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
+}