diff --git a/client/coral-admin/src/components/CommentType.css b/client/coral-admin/src/components/CommentType.css index beafc2a7c..b5dbc229b 100644 --- a/client/coral-admin/src/components/CommentType.css +++ b/client/coral-admin/src/components/CommentType.css @@ -2,12 +2,11 @@ display: inline-block; color: white; background: grey; - height: 32px; box-sizing: border-box; - line-height: 29px; padding: 2px 8px; border-radius: 2px; font-size: 12px; + height: 28px; > i { font-size: 14px; diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index fd5579952..c6ec0fd4c 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -88,15 +88,19 @@ class Comment extends React.Component { } - +
+ + +
- +
Story: {comment.asset.title} {!props.currentAsset && diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index a69100f9d..ce0c31e33 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -7,6 +7,7 @@ import ModerationMenu from './ModerationMenu'; import ModerationHeader from './ModerationHeader'; import ModerationKeysModal from '../../../components/ModerationKeysModal'; import StorySearch from '../containers/StorySearch'; +import Slot from 'coral-framework/components/Slot'; export default class Moderation extends Component { constructor() { @@ -100,7 +101,7 @@ export default class Moderation extends Component { } render () { - const {root, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props; + const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props; const assetId = this.props.params.id; const {asset} = root; @@ -184,6 +185,14 @@ export default class Moderation extends Component { closeSearch={this.closeSearch} storySearchChange={this.props.storySearchChange} /> + +
); } diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index 948f72158..ccc762641 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -493,7 +493,10 @@ span { top: .3em; } -.commentType { +.adminCommentInfoBar { + min-width: 100px; position: absolute; right: 0px; -} + top: 0px; + text-align: right; +} \ No newline at end of file diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 9dd5babcc..f02c148cf 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -38,7 +38,7 @@ function prepareNotificationText(text) { class ModerationContainer extends Component { subscriptions = []; - get activeTab() { + get activeTab() { const {root: {asset, settings}, router, route} = this.props; @@ -47,7 +47,7 @@ class ModerationContainer extends Component { const queue = isPremod(premod) ? 'premod' : 'new'; const activeTab = route.path && route.path !== ':id' ? route.path : queue; - + return activeTab; } diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 4488b0e7f..c9108c0fe 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -129,7 +129,7 @@ export default (document, config = {}) => (WrappedComponent) => { }) .catch((error) => { this.context.eventEmitter.emit(`mutation.${name}.error`, {variables, error}); - throw new error; + throw error; }); }; return config.props({...data, mutate}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 539bd674e..533032412 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -350,16 +350,6 @@ const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => { // adjust the affected user's karma in the next tick. process.nextTick(adjustKarma(Comments, id, status)); - if (status === 'ACCEPTED') { - - // Publish the comment status change via the subscription. - pubsub.publish('commentAccepted', comment); - } else if (status === 'REJECTED') { - - // Publish the comment status change via the subscription. - pubsub.publish('commentRejected', comment); - } - return comment; }; diff --git a/graph/mutators/tag.js b/graph/mutators/tag.js index dc2f96a68..6ce43c772 100644 --- a/graph/mutators/tag.js +++ b/graph/mutators/tag.js @@ -5,7 +5,7 @@ const {ADD_COMMENT_TAG, REMOVE_COMMENT_TAG} = require('../../perms/constants'); /** * Modifies the targeted model with the specified operation to add/remove a tag. */ -const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => { +const modify = async ({user, loaders: {Tags}, pubsub}, operation, {name, id, item_type, asset_id}) => { // Get the global list of tags from the dataloader. const tags = await Tags.getAll.load({id, item_type, asset_id}); diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index dec0553a4..685c26a17 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -31,8 +31,18 @@ const RootMutation = { stopIgnoringUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.stopIgnoringUser({id})); }, - setCommentStatus(_, {id, status}, {mutators: {Comment}}) { - return wrapResponse(null)(Comment.setStatus({id, status})); + async setCommentStatus(_, {id, status}, {mutators: {Comment}, pubsub}) { + const comment = await Comment.setStatus({id, status}); + if (status === 'ACCEPTED') { + + // Publish the comment status change via the subscription. + pubsub.publish('commentAccepted', comment); + } else if (status === 'REJECTED') { + + // Publish the comment status change via the subscription. + pubsub.publish('commentRejected', comment); + } + return wrapResponse(null)(comment); }, addTag(_, {tag}, {mutators: {Tag}}) { return wrapResponse(null)(Tag.add(tag)); diff --git a/plugins/talk-plugin-featured-comments/client/components/ModTag.css b/plugins/talk-plugin-featured-comments/client/components/ModTag.css new file mode 100644 index 000000000..c8ba1b6ba --- /dev/null +++ b/plugins/talk-plugin-featured-comments/client/components/ModTag.css @@ -0,0 +1,42 @@ +.tag { + border: 1px solid #696969; + display: inline-block; + color: #696969; + background-color: white; + box-sizing: border-box; + padding: 2px 8px; + border-radius: 2px; + font-size: 12px; + height: 28px; + transition: background-color .2s cubic-bezier(.4,0,.2,1), color .2s cubic-bezier(.4,0,.2,1), border-color .2s cubic-bezier(.4,0,.2,1); + margin: 2px 0px; + letter-spacing: 0.4px; +} + +.tag:hover { + background-color: #5384B2; + border-color: #5384B2; + color: white; + cursor: pointer; +} + +.tag.featured { + background-color: #10589b; + border-color: #10589b; + color: white; +} + +.tag.featured:hover { + background-color: white; + border-color: #5384B2; + color: #5384B2; + cursor: pointer; +} + + +.tagIcon { + margin-right: 5px; + font-size: 15px; + vertical-align: text-bottom; +} + \ No newline at end of file diff --git a/plugins/talk-plugin-featured-comments/client/components/ModTag.js b/plugins/talk-plugin-featured-comments/client/components/ModTag.js new file mode 100644 index 000000000..a80c3a8a6 --- /dev/null +++ b/plugins/talk-plugin-featured-comments/client/components/ModTag.js @@ -0,0 +1,61 @@ +import React from 'react'; +import cn from 'classnames'; +import styles from './ModTag.css'; +import {t} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components/ui'; +import * as notification from 'coral-admin/src/services/notification'; + +export default class ModTag extends React.Component { + constructor() { + super(); + + this.state = { + on: false + }; + + } + + handleMouseEnter = (e) => { + e.preventDefault(); + this.setState({ + on: true + }); + } + + handleMouseLeave = (e) => { + e.preventDefault(); + this.setState({ + on: false + }); + } + + postTag = async () => { + try { + await this.props.postTag(); + notification.success(t('talk-plugin-featured-comments.notify_self_featured', this.props.comment.user.username)); + } + catch(err) { + notification.showMutationErrors(err); + } + } + + render() { + const {alreadyTagged, deleteTag} = this.props; + + return alreadyTagged ? ( + + + {!this.state.on ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.un_feature')} + + ) : ( + + + {alreadyTagged ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.feature')} + + ); + } +} diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js new file mode 100644 index 000000000..647c7ecfd --- /dev/null +++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js @@ -0,0 +1,110 @@ +import React from 'react'; +import {gql} from 'react-apollo'; +import {connect} from 'react-redux'; +import Comment from 'coral-admin/src/routes/Moderation/containers/Comment'; +import {handleCommentChange} from 'coral-admin/src/graphql/utils'; +import {getDefinitionName} from 'coral-framework/utils'; +import truncate from 'lodash/truncate'; +import t from 'coral-framework/services/i18n'; + +function prepareNotificationText(text) { + return truncate(text, {length: 50}).replace('\n', ' '); +} + +class ModSubscription extends React.Component { + subscriptions = null; + + componentWillMount() { + const configs = [ + { + document: COMMENT_FEATURED_SUBSCRIPTION, + variables: { + assetId: this.props.data.variables.asset_id, + }, + updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => { + const sort = this.props.data.variables.sort; + const text = this.props.user.id === user.id + ? {} + : t( + 'talk-plugin-featured-comments.notify_featured', + user.username, + prepareNotificationText(comment.body), + ); + const notify = { + activeQueue: this.props.activeTab, + text, + anyQueue: true, + }; + return handleCommentChange(prev, comment, sort, notify); + }, + }, + { + document: COMMENT_UNFEATURED_SUBSCRIPTION, + variables: { + assetId: this.props.data.variables.asset_id, + }, + updateQuery: (prev, {subscriptionData: {data: {commentUnfeatured: {user, comment}}}}) => { + const sort = this.props.data.variables.sort; + const text = this.props.user.id === user.id + ? {} + : t( + 'talk-plugin-featured-comments.notify_unfeatured', + user.username, + prepareNotificationText(comment.body), + ); + const notify = { + activeQueue: this.props.activeTab, + text, + anyQueue: true, + }; + return handleCommentChange(prev, comment, sort, notify); + } + }, + ]; + this.subscriptions = configs.map((config) => this.props.data.subscribeToMore(config)); + } + + componentWillUnmount() { + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + } + + render() { + return null; + } +} + +const COMMENT_FEATURED_SUBSCRIPTION = gql` + subscription CommentFeatured($assetId: ID){ + commentFeatured(asset_id: $assetId) { + comment { + ...${getDefinitionName(Comment.fragments.comment)} + } + user { + id + username + } + } + } + ${Comment.fragments.comment} +`; + +const COMMENT_UNFEATURED_SUBSCRIPTION = gql` + subscription CommentUnfeatured($assetId: ID){ + commentUnfeatured(asset_id: $assetId){ + comment { + ...${getDefinitionName(Comment.fragments.comment)} + } + user { + id + username + } + } + } + ${Comment.fragments.comment} +`; + +const mapStateToProps = (state) => ({ + user: state.auth.toJS().user, +}); + +export default connect(mapStateToProps, null)(ModSubscription); diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js new file mode 100644 index 000000000..3b564d127 --- /dev/null +++ b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js @@ -0,0 +1,5 @@ +import ModTag from '../components/ModTag'; +import {withTags} from 'plugin-api/beta/client/hocs'; + +export default withTags('featured')(ModTag); + diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js index 8e4d18058..82cba8b91 100644 --- a/plugins/talk-plugin-featured-comments/client/index.js +++ b/plugins/talk-plugin-featured-comments/client/index.js @@ -5,6 +5,8 @@ import TabPane from './containers/TabPane'; import translations from './translations.yml'; import update from 'immutability-helper'; import reducer from './reducer'; +import ModTag from './containers/ModTag'; +import ModSubscription from './containers/ModSubscription'; import {findCommentInEmbedQuery} from 'coral-embed-stream/src/graphql/utils'; import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; @@ -16,7 +18,9 @@ export default { streamTabs: [Tab], streamTabPanes: [TabPane], commentInfoBar: [Tag], - commentReactions: [Button] + commentReactions: [Button], + adminModeration: [ModSubscription], + adminCommentInfoBar: [ModTag], }, mutations: { IgnoreUser: ({variables}) => ({ diff --git a/plugins/talk-plugin-featured-comments/client/translations.yml b/plugins/talk-plugin-featured-comments/client/translations.yml index 50e51d586..ea504cc9e 100644 --- a/plugins/talk-plugin-featured-comments/client/translations.yml +++ b/plugins/talk-plugin-featured-comments/client/translations.yml @@ -1,12 +1,19 @@ en: talk-plugin-featured-comments: + un_feature: Un-Feature + feature: Feature featured: Featured featured_comments: Featured Comments go_to_conversation: Go to conversation tooltip_description: Comments selected by our team as worth reading + notify_self_featured: 'The comment from {0} is now featured and approved' + notify_featured: '{0} featured and approved comment "{1}"' + notify_unfeatured: '{0} unfeatured comment "{1}"' es: talk-plugin-featured-comments: + un_feature: Desmarcar + feature: Remarcar featured: Remarcado featured_comments: Comentarios Remarcados go_to_conversation: Ir al comentario - tooltip_description: Comentarios seleccionados por nuestro equipo que valen la pena ser leidos \ No newline at end of file + tooltip_description: Comentarios seleccionados por nuestro equipo que valen la pena ser leidos diff --git a/plugins/talk-plugin-featured-comments/index.js b/plugins/talk-plugin-featured-comments/index.js index 88f8344ae..46e3f97fa 100644 --- a/plugins/talk-plugin-featured-comments/index.js +++ b/plugins/talk-plugin-featured-comments/index.js @@ -1,4 +1,87 @@ +const {check} = require('perms/utils'); + module.exports = { + typeDefs: ` + + type CommentFeaturedData { + comment: Comment! + user: User! + } + + type CommentUnfeaturedData { + comment: Comment! + user: User! + } + + type Subscription { + + # Subscribe to featured comments. + commentFeatured(asset_id: ID): CommentFeaturedData + + # Subscribe to featured comments. + commentUnfeatured(asset_id: ID): CommentUnfeaturedData + } + `, + resolvers: { + Subscription: { + commentFeatured: ({user, comment}) => { + return {user, comment}; + }, + commentUnfeatured: ({user, comment}) => { + return {user, comment}; + }, + }, + }, + setupFunctions: { + commentFeatured: (options, args) => ({ + commentFeatured: { + filter: ({comment}, {user}) => { + if (args.asset_id === null) { + return check(user, ['ADMIN', 'MODERATOR']); + } + return comment.asset_id === args.asset_id; + }, + }, + }), + commentUnfeatured: (options, args) => ({ + commentUnfeatured: { + filter: ({comment}, {user}) => { + if (args.asset_id === null) { + return check(user, ['ADMIN', 'MODERATOR']); + } + return comment.asset_id === args.asset_id; + }, + }, + }), + }, + hooks: { + RootMutation: { + addTag: { + async post(obj, {tag: {name, id, item_type}}, {user, mutators: {Comment}, pubsub}, info, result) { + if (name === 'FEATURED' && item_type === 'COMMENTS') { + const comment = await Comment.setStatus({id: id, status: 'ACCEPTED'}); + if (comment) { + pubsub.publish('commentFeatured', {comment, user}); + } + return result; + } + return result; + }, + }, + removeTag: { + async post(obj, {tag: {name, id, item_type}}, {user, loaders: {Comments}, pubsub}, info, result) { + if (name === 'FEATURED' && item_type === 'COMMENTS') { + const comment = await Comments.get.load(id); + if (comment) { + pubsub.publish('commentUnfeatured', {comment, user}); + } + return result; + } + return result; + }, + }, + }, + }, tags: [ { name: 'FEATURED',