mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 03:34:45 +08:00
Merge pull request #826 from coralproject/featured-adm
Feature Comments for Moderators / Admins
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -88,15 +88,19 @@ class Comment extends React.Component {
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
comment={comment}
|
||||
asset={comment.asset}
|
||||
fill="adminCommentInfoBar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentInfoBar"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset &&
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<Slot
|
||||
data={data}
|
||||
root={root}
|
||||
assset={asset}
|
||||
activeTab={activeTab}
|
||||
fill='adminModeration'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -493,7 +493,10 @@ span {
|
||||
top: .3em;
|
||||
}
|
||||
|
||||
.commentType {
|
||||
.adminCommentInfoBar {
|
||||
min-width: 100px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
top: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<span className={cn(styles.tag, styles.featured)}
|
||||
onClick={deleteTag}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave} >
|
||||
<Icon name="star_outline" className={cn(styles.tagIcon)} />
|
||||
{!this.state.on ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.un_feature')}
|
||||
</span>
|
||||
) : (
|
||||
<span className={cn(styles.tag, {[styles.featured]: alreadyTagged})}
|
||||
onClick={this.postTag} >
|
||||
<Icon name="star_outline" className={cn(styles.tagIcon)} />
|
||||
{alreadyTagged ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.feature')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
import ModTag from '../components/ModTag';
|
||||
import {withTags} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
export default withTags('featured')(ModTag);
|
||||
|
||||
@@ -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}) => ({
|
||||
|
||||
@@ -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
|
||||
tooltip_description: Comentarios seleccionados por nuestro equipo que valen la pena ser leidos
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user