mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge branch 'master' into fix-graphql-warnings
This commit is contained in:
@@ -22,6 +22,7 @@ plugins/*
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -39,6 +39,7 @@ plugins/*
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -18,7 +18,7 @@ import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import CommentContent from './CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
import InactiveCommentLabel from './InactiveCommentLabel';
|
||||
import {EditableCommentContent} from './EditableCommentContent';
|
||||
import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive, getShallowChanges} from 'coral-framework/utils';
|
||||
@@ -209,6 +209,10 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
commentIsRejected(comment) {
|
||||
return comment.status === 'REJECTED';
|
||||
}
|
||||
|
||||
commentIsIgnored(comment) {
|
||||
const me = this.props.root.me;
|
||||
return (
|
||||
@@ -337,9 +341,13 @@ export default class Comment extends React.Component {
|
||||
emit,
|
||||
commentClassNames = []
|
||||
} = this.props;
|
||||
|
||||
if (this.commentIsRejected(comment)) {
|
||||
return <CommentTombstone action='reject' />;
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
return <IgnoredCommentTombstone />;
|
||||
return <CommentTombstone action='ignore' />;
|
||||
}
|
||||
|
||||
const view = this.getVisibileReplies();
|
||||
|
||||
+17
-4
@@ -2,8 +2,21 @@ import React from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Render in place of a Comment when the author of the comment is ignored
|
||||
class IgnoredCommentTombstone extends React.Component {
|
||||
// Render in place of a Comment when the author of the comment is <action>
|
||||
class CommentTombstone extends React.Component {
|
||||
getCopy() {
|
||||
const {action} = this.props;
|
||||
|
||||
switch (action) {
|
||||
case 'ignore':
|
||||
return t('framework.comment_is_ignored');
|
||||
case 'reject':
|
||||
return t('framework.comment_is_rejected');
|
||||
default :
|
||||
return t('framework.comment_is_hidden');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
@@ -14,11 +27,11 @@ class IgnoredCommentTombstone extends React.Component {
|
||||
padding: '1em',
|
||||
color: '#3E4F71',
|
||||
}}>
|
||||
{t('framework.comment_is_ignored')}
|
||||
{this.getCopy()}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IgnoredCommentTombstone;
|
||||
export default CommentTombstone;
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {IgnoreUserWizard} from './IgnoreUserWizard';
|
||||
import Toggleable from './Toggleable';
|
||||
|
||||
// TopRightMenu appears as a dropdown in the top right of the comment.
|
||||
// when you click the down cehvron, it expands and shows IgnoreUserWizard
|
||||
// when you click 'cancel' in the wizard, it closes the menu
|
||||
export class TopRightMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
// comment on which this menu appears
|
||||
comment: PropTypes.shape({
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
ignoreUser: PropTypes.func,
|
||||
|
||||
// show notification to the user (e.g. for errors)
|
||||
notify: PropTypes.func.isRequired,
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timesReset: 0
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const {comment, ignoreUser, notify} = this.props;
|
||||
|
||||
// timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
|
||||
const reset = () => this.setState({timesReset: this.state.timesReset + 1});
|
||||
const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => {
|
||||
|
||||
// close menu
|
||||
reset();
|
||||
|
||||
// ignore user
|
||||
try {
|
||||
await ignoreUser({id});
|
||||
} catch (error) {
|
||||
notify('error', 'Failed to ignore user');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Toggleable key={this.state.timesReset} className="talk-stream-comment-chevron">
|
||||
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
|
||||
<IgnoreUserWizard
|
||||
user={comment.user}
|
||||
cancel={reset}
|
||||
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
|
||||
/>
|
||||
</div>
|
||||
</Toggleable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,27 @@ export const withSetCommentStatus = withMutation(
|
||||
commentId,
|
||||
status,
|
||||
},
|
||||
optimisticResponse: {
|
||||
setCommentStatus: {
|
||||
__typename: 'SetCommentStatusResponse',
|
||||
errors: null,
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
|
||||
const fragment = gql`
|
||||
fragment Talk_SetCommentStatus on Comment {
|
||||
status
|
||||
}`;
|
||||
|
||||
const fragmentId = `Comment_${commentId}`;
|
||||
|
||||
const data = proxy.readFragment({fragment, id: fragmentId});
|
||||
|
||||
data.status = status;
|
||||
|
||||
proxy.writeFragment({fragment, id: fragmentId, data});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './IgnoredUsers.css';
|
||||
|
||||
export class IgnoredUsers extends Component {
|
||||
static propTypes = {
|
||||
users: PropTypes.arrayOf(PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
})).isRequired,
|
||||
|
||||
// accepts { id }
|
||||
stopIgnoring: PropTypes.func.isRequired,
|
||||
}
|
||||
render() {
|
||||
const {users, stopIgnoring} = this.props;
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
users.length
|
||||
? <p>{t('framework.because_you_ignored')}</p>
|
||||
: null
|
||||
}
|
||||
<dl className={styles.ignoredUserList}>
|
||||
{
|
||||
users.map(({username, id}) => (
|
||||
<span className={styles.ignoredUser} key={id}>
|
||||
<dt key={id}>{ username }</dt>
|
||||
<dd className={styles.stopListening}>
|
||||
<a
|
||||
onClick={() => stopIgnoring({id})}
|
||||
className={styles.link}>{t('framework.stop_ignoring')}</a>
|
||||
</dd>
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IgnoredUsers;
|
||||
+4
-9
@@ -10,7 +10,7 @@ sidebar:
|
||||
nav: "docs"
|
||||
---
|
||||
|
||||
Online comments are broken. Mozilla's open-source, plugin-based tool Talk rethinks how moderation, comment display, and conversation function online, creating the opportunity for better, smarter discussions designed to fit your needs. [Read more about Talk here.](https://coralproject.net/products/talk)
|
||||
Online comments are broken. Mozilla's open-source, plugin-based tool Talk rethinks how moderation, comment display, and conversation function online, creating the opportunity for better, smarter discussions designed to fit your needs. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
@@ -32,11 +32,7 @@ See our [Contribution Guide]({{ "/docs/development/contributing" | absolute_url
|
||||
|
||||
- iPad
|
||||
- iPad Pro
|
||||
- iPhone 7 Plus
|
||||
- iPhone 7
|
||||
- iPhone 6 Plus
|
||||
- iPhone 6
|
||||
- iPhone 5
|
||||
- iPhone 5 and later
|
||||
|
||||
### iOS Browsers
|
||||
|
||||
@@ -46,9 +42,8 @@ See our [Contribution Guide]({{ "/docs/development/contributing" | absolute_url
|
||||
|
||||
### Android Devices
|
||||
|
||||
- Galaxy S5
|
||||
- Nexus 5X
|
||||
- Nexus 6P
|
||||
- Galaxy S5 and later
|
||||
- Nexus 5X and later
|
||||
|
||||
### Android Browsers
|
||||
|
||||
|
||||
@@ -194,16 +194,32 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
|
||||
* @param {String} [asset_id] id of asset comment is posted on
|
||||
* @return {Object} resolves to the wordlist results
|
||||
*/
|
||||
const filterNewComment = (context, {body, asset_id}) => {
|
||||
const filterNewComment = async (context, {body, asset_id}) => {
|
||||
|
||||
// Load the settings.
|
||||
const [
|
||||
settings,
|
||||
asset,
|
||||
] = await Promise.all([
|
||||
context.loaders.Settings.load(),
|
||||
context.loaders.Assets.getByID.load(asset_id),
|
||||
]);
|
||||
|
||||
// Create a new instance of the Wordlist.
|
||||
const wl = new Wordlist();
|
||||
|
||||
// Load the wordlist.
|
||||
wl.upsert(settings.wordlist);
|
||||
|
||||
// Load the wordlist and filter the comment content.
|
||||
return Promise.all([
|
||||
wl.load().then(() => wl.scan('body', body)),
|
||||
asset_id && AssetsService.rectifySettings(AssetsService.findById(asset_id))
|
||||
]);
|
||||
return [
|
||||
|
||||
// Scan the word.
|
||||
wl.scan('body', body),
|
||||
|
||||
// Return the asset's settings.
|
||||
await AssetsService.rectifySettings(asset, settings)
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -247,7 +263,7 @@ const resolveNewCommentStatus = async (context, {asset_id, body, status}, wordli
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
let {moderation, charCountEnable, charCount} = await AssetsService.rectifySettings(asset);
|
||||
let {moderation, charCountEnable, charCount} = await AssetsService.rectifySettings(asset, settings);
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
@@ -365,7 +381,7 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
|
||||
const status = await resolveNewCommentStatus(context, {asset_id, body}, wordlist, settings);
|
||||
|
||||
// Execute the edit.
|
||||
const comment = await CommentsService.edit(id, context.user.id, {body, status});
|
||||
const comment = await CommentsService.edit({id, author_id: context.user.id, body, status});
|
||||
|
||||
// Publish the edited comment via the subscription.
|
||||
context.pubsub.publish('commentEdited', comment);
|
||||
|
||||
@@ -221,6 +221,8 @@ en:
|
||||
banned_account_body: "This means that you cannot Like, Report, or write comments."
|
||||
comment: comment
|
||||
comment_is_ignored: "This comment is hidden because you ignored this user."
|
||||
comment_is_rejected: "You have rejected this comment."
|
||||
comment_is_hidden: "This comment is not available."
|
||||
comments: comments
|
||||
configure_stream: "Configure"
|
||||
content_not_available: "This content is not available"
|
||||
|
||||
@@ -219,6 +219,8 @@ es:
|
||||
banned_account_body: "Esto significa que no puedes gustar, marcar o escribir comentarios."
|
||||
comment: "comentario"
|
||||
comment_is_ignored: "Este comentario está escondido porque has ignorado al usuario."
|
||||
comment_is_rejected: "Has rechazado este comentario."
|
||||
comment_is_hidden: "Este comentario no está disponible."
|
||||
comments: "comentarios"
|
||||
configure_stream: "Configurar Hilo de Comentarios"
|
||||
content_not_available: "Este contenido no se encuentra disponible"
|
||||
|
||||
@@ -8,4 +8,5 @@ export {default as withEmit} from 'coral-framework/hocs/withEmit';
|
||||
export {
|
||||
withIgnoreUser,
|
||||
withStopIgnoringUser,
|
||||
withSetCommentStatus,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,3 @@ const Button = (props) => {
|
||||
};
|
||||
|
||||
export default withTags('featured')(Button);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {t, timeago} from 'plugin-api/beta/client/services';
|
||||
import {Slot, CommentAuthorName} from 'plugin-api/beta/client/components';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import {pluginName} from '../../package.json';
|
||||
import Button from './Button';
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
@@ -48,6 +49,13 @@ class Comment extends React.Component {
|
||||
asset={asset}
|
||||
inline
|
||||
/>
|
||||
|
||||
<Button
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(styles.actionsContainer, `${pluginName}-comment-actions`)}>
|
||||
<button className={cn(styles.goTo, `${pluginName}-comment-go-to`)} onClick={this.viewComment}>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
transition: color 100ms, background-color 100ms;
|
||||
border-radius: 3px;
|
||||
color: #383A43;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #D8D8D8;
|
||||
color: #383a43;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.button.featured {
|
||||
color: #10589b;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: #D8D8D8;
|
||||
color: #383a43;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './ModActionButton.css';
|
||||
import {pluginName} from '../../package.json';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {withTags} from 'plugin-api/beta/client/hocs';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
export class Button 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
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {alreadyTagged, deleteTag, postTag} = this.props;
|
||||
|
||||
return (
|
||||
<button className={cn(`${pluginName}-tag-button`, styles.button, {[styles.featured] : alreadyTagged})}
|
||||
onClick={alreadyTagged ? deleteTag : postTag}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave} >
|
||||
|
||||
{alreadyTagged ? (
|
||||
<span className={styles.approved}>
|
||||
<Icon name="star" className={styles.icon} />
|
||||
{!this.state.on ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.un_feature')}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="star_border" className={styles.icon} />
|
||||
{t('talk-plugin-featured-comments.feature')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTags('featured')(Button);
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
top: 36px;
|
||||
top: 33px;
|
||||
left: auto;
|
||||
right: 10px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.tooltip::before{
|
||||
@@ -48,4 +48,8 @@
|
||||
left: auto;
|
||||
right: 16px;
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
.tagContainer {
|
||||
position: relative;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import cn from 'classnames';
|
||||
import styles from './Tag.css';
|
||||
import Tooltip from './Tooltip';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {isTagged} from 'plugin-api/beta/client/utils';
|
||||
|
||||
export default class Tag extends React.Component {
|
||||
constructor() {
|
||||
@@ -32,19 +31,14 @@ export default class Tag extends React.Component {
|
||||
render() {
|
||||
const {tooltip} = this.state;
|
||||
return(
|
||||
<div className={styles.noSelect} onMouseEnter={this.showTooltip}
|
||||
<span className={cn(styles.tagContainer, styles.noSelect)} onMouseEnter={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
|
||||
onTouchEnd={this.hideTooltip}>
|
||||
{
|
||||
isTagged(this.props.comment.tags, 'FEATURED') ? (
|
||||
<span
|
||||
className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
|
||||
{t('talk-plugin-featured-comments.featured')}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
onTouchEnd={this.hideTooltip} >
|
||||
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
|
||||
{t('talk-plugin-featured-comments.featured')}
|
||||
</span>
|
||||
{tooltip && <Tooltip className={styles.tooltip} />}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {compose} from 'react-apollo';
|
||||
import {excludeIf} from 'plugin-api/beta/client/hocs';
|
||||
import {can} from 'plugin-api/beta/client/services';
|
||||
import Button from '../components/Button';
|
||||
|
||||
const enhance = compose(
|
||||
excludeIf((props) => !can(props.user, 'MODERATE_COMMENTS')),
|
||||
);
|
||||
|
||||
export default enhance(Button);
|
||||
@@ -1,15 +1,18 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import Tag from '../components/Tag';
|
||||
import {withFragments} from 'plugin-api/beta/client/hocs';
|
||||
import {isTagged} from 'plugin-api/beta/client/utils';
|
||||
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
export default withFragments({
|
||||
comment: gql`
|
||||
fragment TalkFeaturedComments_Tag_comment on Comment {
|
||||
tags {
|
||||
tag {
|
||||
name
|
||||
export default compose(
|
||||
withFragments({
|
||||
comment: gql`
|
||||
fragment TalkFeaturedComments_Tag_comment on Comment {
|
||||
tags {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})(Tag);
|
||||
`}),
|
||||
excludeIf((props) => !isTagged(props.comment.tags, 'FEATURED'))
|
||||
)(Tag);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Tab from './containers/Tab';
|
||||
import Tag from './containers/Tag';
|
||||
import Button from './components/Button';
|
||||
import ModActionButton from './components/ModActionButton';
|
||||
import TabPane from './containers/TabPane';
|
||||
import translations from './translations.yml';
|
||||
import update from 'immutability-helper';
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
streamTabs: [Tab],
|
||||
streamTabPanes: [TabPane],
|
||||
commentInfoBar: [Tag],
|
||||
commentReactions: [Button],
|
||||
moderationActions: [ModActionButton],
|
||||
adminModeration: [ModSubscription],
|
||||
adminCommentInfoBar: [ModTag],
|
||||
},
|
||||
@@ -49,25 +49,39 @@ export default {
|
||||
AddTag: ({variables}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (previous) => {
|
||||
let updated = previous;
|
||||
|
||||
if (variables.name !== 'FEATURED') {
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = findCommentInEmbedQuery(previous, variables.id);
|
||||
|
||||
const updated = update(previous, {
|
||||
asset: {
|
||||
featuredComments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => prependNewNodes(nodes, [comment]),
|
||||
}
|
||||
},
|
||||
featuredCommentsCount: {
|
||||
$apply: (value) => value + 1
|
||||
|
||||
if (previous.asset.comments) {
|
||||
updated = update(previous, {
|
||||
asset: {
|
||||
comments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => nodes.map((node) => {
|
||||
if (node.id === variables.id) {
|
||||
node.status = 'ACCEPTED';
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
}
|
||||
},
|
||||
featuredComments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => prependNewNodes(nodes, [comment]),
|
||||
}
|
||||
},
|
||||
featuredCommentsCount: {
|
||||
$apply: (value) => value + 1
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
},
|
||||
@@ -76,24 +90,27 @@ export default {
|
||||
RemoveTag: ({variables}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (previous) => {
|
||||
|
||||
let updated = previous;
|
||||
|
||||
if (variables.name !== 'FEATURED') {
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = update(previous, {
|
||||
asset: {
|
||||
featuredComments: {
|
||||
nodes: {
|
||||
$apply: (nodes) =>
|
||||
nodes.filter((n) => n.id !== variables.id)
|
||||
if (previous.asset.comments) {
|
||||
updated = update(previous, {
|
||||
asset: {
|
||||
featuredComments: {
|
||||
nodes: {
|
||||
$apply: (nodes) =>
|
||||
nodes.filter((n) => n.id !== variables.id)
|
||||
}
|
||||
},
|
||||
featuredCommentsCount: {
|
||||
$apply: (value) => value - 1
|
||||
}
|
||||
},
|
||||
featuredCommentsCount: {
|
||||
$apply: (value) => value - 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
const isApproved = (status) => (status === 'ACCEPTED');
|
||||
|
||||
export default ({approveComment, comment: {status}}) => (
|
||||
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={approveComment}>
|
||||
{isApproved(status) ? (
|
||||
<span className={styles.approved}>
|
||||
<Icon name="check_circle" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.approved_comment')}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="done" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.approve_comment')}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
.moderationActions {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
-ms-user-select:none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout:none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import Tooltip from './Tooltip';
|
||||
import styles from './ModerationActions.css';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import RejectCommentAction from '../containers/RejectCommentAction';
|
||||
import ApproveCommentAction from '../containers/ApproveCommentAction';
|
||||
import {Slot} from 'plugin-api/beta/client/components';
|
||||
|
||||
export default class ModerationActions extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
tooltip: false
|
||||
};
|
||||
}
|
||||
|
||||
toogleTooltip = () => {
|
||||
const {tooltip} = this.state;
|
||||
this.setState({
|
||||
tooltip: !tooltip
|
||||
});
|
||||
}
|
||||
|
||||
hideTooltip = () => {
|
||||
this.setState({
|
||||
tooltip: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {tooltip} = this.state;
|
||||
const {comment, asset, data} = this.props;
|
||||
|
||||
return(
|
||||
<ClickOutside onClickOutside={this.hideTooltip}>
|
||||
<div className={cn(styles.moderationActions, 'talk-plugin-moderation-actions')}>
|
||||
<span onClick={this.toogleTooltip} className={cn(styles.arrow, 'talk-plugin-moderation-actions-arrow')}>
|
||||
{tooltip ? <Icon name="keyboard_arrow_up" className={styles.icon} /> :
|
||||
<Icon name="keyboard_arrow_down" className={styles.icon} />}
|
||||
</span>
|
||||
{tooltip && (
|
||||
<Tooltip>
|
||||
|
||||
<Slot
|
||||
className="talk-plugin-modetarion-actions-slot"
|
||||
fill="moderationActions"
|
||||
queryData={{comment, asset}}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<ApproveCommentAction comment={comment} />
|
||||
<RejectCommentAction comment={comment} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
export default ({rejectComment}) => (
|
||||
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={rejectComment}>
|
||||
<Icon name="clear" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.reject_comment')}
|
||||
</button>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
.tooltip {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
right: 0px;
|
||||
width: 140px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.tooltip::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.tooltip::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.headline {
|
||||
color: #484747;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 4px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './Tooltip.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
export default ({className = '', children}) => (
|
||||
<div className={cn(styles.tooltip, className)}>
|
||||
<h3 className={styles.headline}>
|
||||
{t('talk-plugin-moderation-actions.moderation_actions')}
|
||||
</h3>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
.root {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
transition: color 100ms, background-color 100ms;
|
||||
border-radius: 3px;
|
||||
color: #383A43;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #D8D8D8;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.approved {
|
||||
color: #519954;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import {getErrorMessages} from 'plugin-api/beta/client/utils';
|
||||
import {withSetCommentStatus} from 'plugin-api/beta/client/hocs';
|
||||
import {notify} from 'plugin-api/beta/client/actions/notification';
|
||||
import ApproveCommentAction from '../components/ApproveCommentAction';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
class ApproveCommentActionContainer extends React.Component {
|
||||
|
||||
approveComment = async () => {
|
||||
const {setCommentStatus, comment} = this.props;
|
||||
|
||||
try {
|
||||
const result = await setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: 'ACCEPTED'
|
||||
});
|
||||
|
||||
if (!isNil(result.data.setCommentStatus)) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ApproveCommentAction comment={this.props.comment} approveComment={this.approveComment}/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default withSetCommentStatus(ApproveCommentActionContainer);
|
||||
@@ -0,0 +1,32 @@
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {can} from 'plugin-api/beta/client/services';
|
||||
import ModerationActions from '../components/ModerationActions';
|
||||
import {connect, excludeIf, withFragments} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
const mapStateToProps = ({auth}) => ({
|
||||
user: auth.user
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps),
|
||||
withFragments({
|
||||
asset: gql`
|
||||
fragment TalkModerationActions_asset on Asset {
|
||||
id
|
||||
}`
|
||||
,
|
||||
comment: gql`
|
||||
fragment TalkModerationActions_comment on Comment {
|
||||
id
|
||||
status
|
||||
tags {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`}),
|
||||
excludeIf((props) => !can(props.user, 'MODERATE_COMMENTS')),
|
||||
);
|
||||
|
||||
export default enhance(ModerationActions);
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import {getErrorMessages} from 'plugin-api/beta/client/utils';
|
||||
import {withSetCommentStatus} from 'plugin-api/beta/client/hocs';
|
||||
import {notify} from 'plugin-api/beta/client/actions/notification';
|
||||
import RejectCommentAction from '../components/RejectCommentAction';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
class RejectCommentActionContainer extends React.Component {
|
||||
|
||||
rejectComment = async () => {
|
||||
const {setCommentStatus, comment} = this.props;
|
||||
|
||||
try {
|
||||
const result = await setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: 'REJECTED'
|
||||
});
|
||||
|
||||
if (!isNil(result.data.setCommentStatus)) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <RejectCommentAction rejectComment={this.rejectComment}/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default withSetCommentStatus(RejectCommentActionContainer);
|
||||
@@ -0,0 +1,9 @@
|
||||
import ModerationActions from './containers/ModerationActions';
|
||||
import translations from './translations.yml';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentInfoBar: [ModerationActions],
|
||||
},
|
||||
translations
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
en:
|
||||
talk-plugin-moderation-actions:
|
||||
reject_comment: "Reject"
|
||||
approve_comment: "Approve"
|
||||
approved_comment: "Approved"
|
||||
moderation_actions: "Moderation Actions"
|
||||
es:
|
||||
talk-plugin-moderation-actions:
|
||||
reject_comment: "Rechazar"
|
||||
approve_comment: "Aprobar"
|
||||
approved_comment: "Aprobado"
|
||||
moderation_actions: "Acciones de Moderación"
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
+16
-11
@@ -3,6 +3,7 @@ const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
const domainlist = require('./domainlist');
|
||||
const errors = require('../errors');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
module.exports = class AssetsService {
|
||||
|
||||
@@ -28,19 +29,23 @@ module.exports = class AssetsService {
|
||||
* @param {Promise} assetQuery an asset query that returns a single asset.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static rectifySettings(assetQuery) {
|
||||
return Promise.all([
|
||||
SettingsService.retrieve(),
|
||||
assetQuery
|
||||
]).then(([settings, asset]) => {
|
||||
static async rectifySettings(assetQuery, settings = null) {
|
||||
const [
|
||||
globalSettings,
|
||||
asset,
|
||||
] = await Promise.all([
|
||||
settings !== null ? settings : SettingsService.retrieve(),
|
||||
assetQuery,
|
||||
]);
|
||||
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
settings = merge({}, globalSettings, asset.settings);
|
||||
} else {
|
||||
settings = globalSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+71
-32
@@ -5,6 +5,7 @@ const ActionsService = require('./actions');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
const sc = require('snake-case');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
const errors = require('../errors');
|
||||
const events = require('./events');
|
||||
const {
|
||||
@@ -52,13 +53,34 @@ module.exports = class CommentsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a Comment
|
||||
* @param {String} id comment.id you want to edit (or its ID)
|
||||
* @param {String} author_id user.id of the user trying to edit the comment (will err if not comment author)
|
||||
* lastUnmoderatedStatus will retrieve the last status before this one.
|
||||
*
|
||||
* @param {Object} comment the comment to get the last status of
|
||||
*/
|
||||
static lastUnmoderatedStatus(comment) {
|
||||
const UNMODERATED_STATUSES = [
|
||||
'NONE',
|
||||
'PREMOD',
|
||||
];
|
||||
|
||||
for (let i = comment.status_history.length - 1; i >= 0; i--) {
|
||||
const {type} = comment.status_history[i];
|
||||
|
||||
if (UNMODERATED_STATUSES.includes(type)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a Comment.
|
||||
*
|
||||
* @param {String} id comment.id you want to edit (or its ID)
|
||||
* @param {String} author_id user.id of the user trying to edit the comment (will err if not comment author)
|
||||
* @param {String} body the new Comment body
|
||||
* @param {String} status the new Comment status
|
||||
*/
|
||||
static async edit(id, author_id, {body, status, ignoreEditWindow = false}) {
|
||||
static async edit({id, author_id, body, status}) {
|
||||
const query = {
|
||||
id,
|
||||
author_id,
|
||||
@@ -69,14 +91,11 @@ module.exports = class CommentsService {
|
||||
|
||||
// Establish the edit window (if it exists) and add the condition to the
|
||||
// original query.
|
||||
let lastEditableCommentCreatedAt;
|
||||
if (!ignoreEditWindow) {
|
||||
const {editCommentWindowLength: editWindowMs} = await SettingsService.retrieve();
|
||||
lastEditableCommentCreatedAt = new Date((new Date()).getTime() - editWindowMs);
|
||||
query.created_at = {
|
||||
$gt: lastEditableCommentCreatedAt,
|
||||
};
|
||||
}
|
||||
const {editCommentWindowLength: editWindowMs} = await SettingsService.retrieve();
|
||||
const lastEditableCommentCreatedAt = new Date(Date.now() - editWindowMs);
|
||||
query.created_at = {
|
||||
$gt: lastEditableCommentCreatedAt,
|
||||
};
|
||||
|
||||
const originalComment = await CommentModel.findOneAndUpdate(query, {
|
||||
$set: {
|
||||
@@ -117,7 +136,7 @@ module.exports = class CommentsService {
|
||||
}
|
||||
|
||||
// Check to see if the edit window expired.
|
||||
if (!ignoreEditWindow && comment.created_at <= lastEditableCommentCreatedAt) {
|
||||
if (comment.created_at <= lastEditableCommentCreatedAt) {
|
||||
debug('rejecting comment edit because outside edit time window');
|
||||
throw errors.ErrEditWindowHasEnded;
|
||||
}
|
||||
@@ -126,7 +145,7 @@ module.exports = class CommentsService {
|
||||
}
|
||||
|
||||
// Mutate the comment like Mongo would have.
|
||||
const editedComment = originalComment;
|
||||
const editedComment = cloneDeep(originalComment);
|
||||
editedComment.status = status;
|
||||
editedComment.body = body;
|
||||
editedComment.body_history.push({
|
||||
@@ -138,6 +157,43 @@ module.exports = class CommentsService {
|
||||
created_at: new Date(),
|
||||
});
|
||||
|
||||
// We should adjust the comment's status such that if it was approved
|
||||
// previously, we should mark the comment as 'NONE' or 'PREMOD', which ever
|
||||
// was most recent if the new comment is destined to be `NONE` or `PREMOD`.
|
||||
if (originalComment.status === 'ACCEPTED' && status === 'NONE') {
|
||||
|
||||
const lastUnmoderatedStatus = CommentsService.lastUnmoderatedStatus(originalComment);
|
||||
|
||||
// If the last moderated status was found and the current comment doesn't
|
||||
// match this already.
|
||||
if (lastUnmoderatedStatus && status !== lastUnmoderatedStatus) {
|
||||
|
||||
// Update the comment model (if at this point, the status is still
|
||||
// accepted) with the previously unmoderated status
|
||||
await CommentModel.update({
|
||||
id,
|
||||
status,
|
||||
}, {
|
||||
$set: {
|
||||
status: lastUnmoderatedStatus,
|
||||
},
|
||||
$push: {
|
||||
status_history: {
|
||||
type: lastUnmoderatedStatus,
|
||||
created_at: new Date(),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Update the returned comment.
|
||||
editedComment.status = lastUnmoderatedStatus;
|
||||
editedComment.status_history.push({
|
||||
type: lastUnmoderatedStatus,
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment);
|
||||
|
||||
return editedComment;
|
||||
@@ -215,23 +271,6 @@ module.exports = class CommentsService {
|
||||
return CommentModel.find({status});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find comments that need to be moderated (aka moderation queue).
|
||||
* @param {String} asset_id
|
||||
* @return {Promise}
|
||||
*/
|
||||
static moderationQueue(status = 'NONE', asset_id = null) {
|
||||
|
||||
// Fetch the comments with statuses.
|
||||
let comments = CommentModel.find({status});
|
||||
|
||||
if (asset_id) {
|
||||
comments = comments.where({asset_id});
|
||||
}
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a new status in for the user.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
@@ -356,7 +395,7 @@ events.on(COMMENTS_NEW, async (comment) => {
|
||||
if (
|
||||
!comment || // Check that the comment is defined.
|
||||
(!comment.parent_id || comment.parent_id.length === 0) || // Check that the comment has a parent (is a reply).
|
||||
!(comment.status === 'NONE' || comment.status === 'APPROVED') // Check that the comment is visible.
|
||||
!(comment.status === 'NONE' || comment.status === 'ACCEPTED') // Check that the comment is visible.
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ const CommentsService = require('../../../services/comments');
|
||||
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('sinon-chai'));
|
||||
const expect = chai.expect;
|
||||
|
||||
@@ -96,102 +95,118 @@ describe('services.CommentsService', () => {
|
||||
user_id: '456'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
ActionModel.create(actions)
|
||||
]);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init(settings);
|
||||
|
||||
await Promise.all([
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
ActionModel.create(actions)
|
||||
]);
|
||||
});
|
||||
|
||||
describe('#publicCreate()', () => {
|
||||
|
||||
it('creates a new comment', () => {
|
||||
return CommentsService
|
||||
.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
}).then((c) => {
|
||||
expect(c).to.not.be.null;
|
||||
expect(c.id).to.not.be.null;
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.be.equal('ACCEPTED');
|
||||
});
|
||||
it('creates a new comment', async () => {
|
||||
const c = await CommentsService.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
});
|
||||
|
||||
expect(c).to.not.be.null;
|
||||
expect(c.id).to.not.be.null;
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.be.equal('ACCEPTED');
|
||||
});
|
||||
|
||||
it('creates many new comments', () => {
|
||||
return CommentsService
|
||||
.publicCreate([{
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
}, {
|
||||
body: 'This is another comment!'
|
||||
}, {
|
||||
body: 'This is a rejected comment!',
|
||||
status: 'REJECTED'
|
||||
}]).then(([c1, c2, c3]) => {
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.id).to.be.uuid;
|
||||
expect(c1.status).to.be.equal('ACCEPTED');
|
||||
it('creates many new comments', async () => {
|
||||
const [
|
||||
c1,
|
||||
c2,
|
||||
c3,
|
||||
] = await CommentsService.publicCreate([{
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
}, {
|
||||
body: 'This is another comment!'
|
||||
}, {
|
||||
body: 'This is a rejected comment!',
|
||||
status: 'REJECTED'
|
||||
}]);
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.id).to.be.uuid;
|
||||
expect(c2.status).to.be.equal('NONE');
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.status).to.be.equal('ACCEPTED');
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.id).to.be.uuid;
|
||||
expect(c3.status).to.be.equal('REJECTED');
|
||||
});
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.status).to.be.equal('NONE');
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.status).to.be.equal('REJECTED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#edit', () => {
|
||||
it('changes the comment status back to premod if it was accepted', async () => {
|
||||
const originalComment = await CommentsService.publicCreate({
|
||||
body: 'this is a body!',
|
||||
status: 'PREMOD',
|
||||
author_id: '123',
|
||||
});
|
||||
|
||||
expect(originalComment.status_history).to.have.length(1);
|
||||
|
||||
await CommentsService.pushStatus(originalComment.id, 'ACCEPTED');
|
||||
|
||||
let retrivedComment = await CommentsService.findById(originalComment.id);
|
||||
|
||||
expect(retrivedComment).to.have.property('status', 'ACCEPTED');
|
||||
expect(retrivedComment.status_history).to.have.length(2);
|
||||
expect(retrivedComment.status_history[1]).to.have.property('type', 'ACCEPTED');
|
||||
|
||||
const editedComment = await CommentsService.edit({
|
||||
id: originalComment.id,
|
||||
author_id: '123',
|
||||
body: 'This is a body!',
|
||||
status: 'NONE',
|
||||
});
|
||||
|
||||
expect(editedComment).to.have.property('status', 'PREMOD');
|
||||
expect(editedComment.status_history).to.have.length(4);
|
||||
expect(editedComment.status_history[3]).to.have.property('type', 'PREMOD');
|
||||
|
||||
retrivedComment = await CommentsService.findById(originalComment.id);
|
||||
|
||||
expect(retrivedComment).to.have.property('status', 'PREMOD');
|
||||
expect(retrivedComment.status_history).to.have.length(4);
|
||||
expect(retrivedComment.status_history[3]).to.have.property('type', 'PREMOD');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findById()', () => {
|
||||
|
||||
it('should find a comment by id', () => {
|
||||
return CommentsService
|
||||
.findById('1')
|
||||
.then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.property('body', 'comment 10');
|
||||
});
|
||||
it('should find a comment by id', async () => {
|
||||
const comment = await CommentsService.findById('1');
|
||||
expect(comment).to.not.be.null;
|
||||
expect(comment).to.have.property('body', 'comment 10');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#findByAssetId()', () => {
|
||||
|
||||
it('should find an array of all comments by asset id', () => {
|
||||
return CommentsService
|
||||
.findByAssetId('123')
|
||||
.then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
result.sort((a, b) => {
|
||||
if (a.body < b.body) {return -1;}
|
||||
else {return 1;}
|
||||
});
|
||||
expect(result[0]).to.have.property('body', 'comment 10');
|
||||
expect(result[1]).to.have.property('body', 'comment 20');
|
||||
expect(result[2]).to.have.property('body', 'comment 40');
|
||||
});
|
||||
it('should find an array of all comments by asset id', async () => {
|
||||
const comments = await CommentsService.findByAssetId('123');
|
||||
expect(comments).to.have.length(3);
|
||||
comments.sort((a, b) => {
|
||||
if (a.body < b.body) {return -1;}
|
||||
else {return 1;}
|
||||
});
|
||||
expect(comments[0]).to.have.property('body', 'comment 10');
|
||||
expect(comments[1]).to.have.property('body', 'comment 20');
|
||||
expect(comments[2]).to.have.property('body', 'comment 40');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#moderationQueue()', () => {
|
||||
|
||||
it('should find an array of new comments to moderate when pre-moderation', () => {
|
||||
return CommentsService
|
||||
.moderationQueue('PREMOD')
|
||||
.then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#changeStatus', () => {
|
||||
|
||||
it('should change the status of a comment from no status', async () => {
|
||||
@@ -220,20 +235,18 @@ describe('services.CommentsService', () => {
|
||||
expect(c3.status_history[0]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
|
||||
it('should change the status of a comment from accepted', () => {
|
||||
return CommentsService.pushStatus(comments[1].id, 'REJECTED', '123')
|
||||
.then(() => CommentsService.findById(comments[1].id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('REJECTED');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'ACCEPTED');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
it('should change the status of a comment from accepted', async () => {
|
||||
await CommentsService.pushStatus(comments[1].id, 'REJECTED', '123');
|
||||
const c = await CommentsService.findById(comments[1].id);
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('REJECTED');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'ACCEPTED');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
|
||||
expect(c.status_history[1]).to.have.property('type', 'REJECTED');
|
||||
expect(c.status_history[1]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
expect(c.status_history[1]).to.have.property('type', 'REJECTED');
|
||||
expect(c.status_history[1]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user