diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js
index c912d1a72..165b3ce4c 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/components/Stream.js
@@ -57,7 +57,10 @@ class Stream extends React.Component {
constructor(props) {
super(props);
- this.state = resetCursors(this.state, props);
+ this.state = {
+ ...resetCursors(this.state, props),
+ keepCommentBox: false,
+ };
}
componentWillReceiveProps(next) {
@@ -68,6 +71,12 @@ class Stream extends React.Component {
this.setState(resetCursors);
return;
}
+
+ // Keep comment box when user was live suspended, banned, ...
+ if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) {
+ this.setState({keepCommentBox: true});
+ }
+
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
@@ -133,6 +142,10 @@ class Stream extends React.Component {
return view;
}
+ userIsDegraged({auth: {user}} = this.props) {
+ return !can(user, 'INTERACT_WITH_COMMUNITY');
+ }
+
render() {
const {
commentClassNames,
@@ -150,6 +163,7 @@ class Stream extends React.Component {
pluginProps,
editName
} = this.props;
+ const {keepCommentBox} = this.state;
const view = this.getVisibleComments();
const open = asset.closedAt === null;
@@ -171,6 +185,8 @@ class Stream extends React.Component {
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
+
+ const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
return (
@@ -199,10 +215,7 @@ class Stream extends React.Component {
editName={editName}
currentUsername={user.username}
/>}
- {loggedIn &&
- !banned &&
- !temporarilySuspended &&
- !highlightedComment &&
+ {showCommentBox &&
}
diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js
index d65722b6d..4cd544379 100644
--- a/client/coral-embed-stream/src/components/TopRightMenu.js
+++ b/client/coral-embed-stream/src/components/TopRightMenu.js
@@ -12,7 +12,7 @@ export class TopRightMenu extends React.Component {
comment: PropTypes.shape({
user: PropTypes.shape({
id: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired
+ username: PropTypes.string.isRequired
}).isRequired
}).isRequired,
ignoreUser: PropTypes.func,
diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js
index f5682bdd1..040d1a83e 100644
--- a/client/coral-embed-stream/src/containers/Comment.js
+++ b/client/coral-embed-stream/src/containers/Comment.js
@@ -34,7 +34,7 @@ export default withFragments({
}
user {
id
- name: username
+ username
}
action_summaries {
__typename
diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js
index 68f76ccc5..bf05caf29 100644
--- a/client/coral-embed-stream/src/containers/Embed.js
+++ b/client/coral-embed-stream/src/containers/Embed.js
@@ -12,6 +12,8 @@ import {getDefinitionName} from 'coral-framework/utils';
import {withQuery} from 'coral-framework/hocs';
import Embed from '../components/Embed';
import Stream from './Stream';
+import {addNotification} from 'coral-framework/actions/notification';
+import t from 'coral-framework/services/i18n';
import {setActiveTab} from '../actions/embed';
import {viewAllComments} from '../actions/stream';
@@ -20,12 +22,63 @@ const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
+ subscriptions = [];
+
+ subscribeToUpdates(props = this.props) {
+ if (props.auth.loggedIn) {
+ const newSubscriptions = [{
+ document: USER_BANNED_SUBSCRIPTION,
+ updateQuery: () => {
+ addNotification('info', t('your_account_has_been_banned'));
+ },
+ },
+ {
+ document: USER_SUSPENDED_SUBSCRIPTION,
+ updateQuery: () => {
+ addNotification('info', t('your_account_has_been_suspended'));
+ },
+ },
+ {
+ document: USERNAME_REJECTED_SUBSCRIPTION,
+ updateQuery: () => {
+ addNotification('info', t('your_username_has_been_rejected'));
+ },
+ }];
+
+ this.subscriptions = newSubscriptions.map((s) => props.data.subscribeToMore({
+ document: s.document,
+ variables: {
+ user_id: props.auth.user.id,
+ },
+ updateQuery: s.updateQuery,
+ }));
+ }
+ }
+
+ unsubscribe() {
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
+ this.subscriptions = [];
+ }
+
+ resubscribe(props) {
+ this.unsubscribe();
+ this.subscribeToUpdates(props);
+ }
+
+ componentDidMount() {
+ this.subscribeToUpdates();
+ }
+
+ componentWillUnmount() {
+ this.unsubscribe();
+ }
componentWillReceiveProps(nextProps) {
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
// Refetch after login/logout.
this.props.data.refetch();
+ this.resubscribe(nextProps);
}
const {fetchAssetSuccess} = this.props;
@@ -52,6 +105,45 @@ class EmbedContainer extends React.Component {
}
}
+const USER_BANNED_SUBSCRIPTION = gql`
+ subscription UserBanned($user_id: ID!) {
+ userBanned(user_id: $user_id){
+ id
+ status
+ canEditName
+ suspension {
+ until
+ }
+ }
+ }
+`;
+
+const USER_SUSPENDED_SUBSCRIPTION = gql`
+ subscription UserSuspended($user_id: ID!) {
+ userSuspended(user_id: $user_id){
+ id
+ status
+ canEditName
+ suspension {
+ until
+ }
+ }
+ }
+`;
+
+const USERNAME_REJECTED_SUBSCRIPTION = gql`
+ subscription UsernameRejected($user_id: ID!) {
+ usernameRejected(user_id: $user_id){
+ id
+ status
+ canEditName
+ suspension {
+ until
+ }
+ }
+ }
+`;
+
const EMBED_QUERY = gql`
query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
@@ -95,6 +187,7 @@ const mapDispatchToProps = (dispatch) =>
setActiveTab,
viewAllComments,
fetchAssetSuccess,
+ addNotification,
},
dispatch
);
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js
index f6e1fd9ec..3491fc846 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/containers/Stream.js
@@ -30,11 +30,8 @@ class StreamContainer extends React.Component {
subscriptions = [];
subscribeToUpdates() {
- const sub1 = this.props.data.subscribeToMore({
+ const newSubscriptions = [{
document: COMMENTS_EDITED_SUBSCRIPTION,
- variables: {
- assetId: this.props.root.asset.id,
- },
updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
// Ignore mutations from me.
@@ -52,13 +49,9 @@ class StreamContainer extends React.Component {
return removeCommentFromEmbedQuery(prev, commentEdited.id);
}
},
- });
-
- const sub2 = this.props.data.subscribeToMore({
+ },
+ {
document: COMMENTS_ADDED_SUBSCRIPTION,
- variables: {
- assetId: this.props.root.asset.id,
- },
updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
// Ignore mutations from me.
@@ -81,9 +74,15 @@ class StreamContainer extends React.Component {
return insertCommentIntoEmbedQuery(prev, commentAdded);
}
- });
+ }];
- this.subscriptions.push(sub1, sub2);
+ this.subscriptions = newSubscriptions.map((s) => this.props.data.subscribeToMore({
+ document: s.document,
+ variables: {
+ assetId: this.props.root.asset.id,
+ },
+ updateQuery: s.updateQuery,
+ }));
}
unsubscribe() {
@@ -173,7 +172,7 @@ const commentFragment = gql`
`;
const COMMENTS_ADDED_SUBSCRIPTION = gql`
- subscription onCommentAdded($assetId: ID!, $excludeIgnored: Boolean){
+ subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean){
commentAdded(asset_id: $assetId){
parent {
id
@@ -185,7 +184,7 @@ const COMMENTS_ADDED_SUBSCRIPTION = gql`
`;
const COMMENTS_EDITED_SUBSCRIPTION = gql`
- subscription onCommentEdited($assetId: ID!){
+ subscription CommentEdited($assetId: ID!){
commentEdited(asset_id: $assetId){
id
body
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css
index 701a06fd5..a752493a3 100644
--- a/client/coral-embed-stream/style/default.css
+++ b/client/coral-embed-stream/style/default.css
@@ -335,33 +335,6 @@ button.comment__action-button[disabled],
display: inline-block;
}
-.coral-plugin-permalinks-container {
- /*position: relative;*/
- z-index: 2;
-}
-
-.coral-plugin-permalinks-popover {
- display: none;
- background-color: white;
- border: 1px solid black;
- position: absolute;
- padding: 5px;
-}
-
-.coral-plugin-permalinks-popover.active {
- display: block;
-}
-
-.coral-plugin-permalinks-copy-field {
- display: block;
- width: calc(100% - 5px);
-}
-
-.coral-plugin-permalinks-copied-text {
- float: right;
- margin: 8px;
-}
-
/* Flag Styles */
.coral-plugin-flags-popup-form {
diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js
index 5b42fa541..82eea8ef7 100644
--- a/client/coral-embed/src/index.js
+++ b/client/coral-embed/src/index.js
@@ -28,6 +28,7 @@ const snackbarStyles = {
// This function should return value of window.Coral
const Coral = {};
const Talk = (Coral.Talk = {});
+let notificationTimeout = null;
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, query) {
@@ -110,14 +111,15 @@ function configurePymParent(pymParent, opts) {
snackbar.className = `coral-notif-${type}`;
snackbar.textContent = text;
- setTimeout(() => {
+ clearTimeout(notificationTimeout);
+ notificationTimeout = setTimeout(() => {
snackbar.style.transform = 'translate(-50%, 0)';
snackbar.style.opacity = 1;
- }, 0);
- setTimeout(() => {
- snackbar.style.opacity = 0;
- }, 5000);
+ notificationTimeout = setTimeout(() => {
+ snackbar.style.opacity = 0;
+ }, 7000);
+ }, 0);
});
// Helps child show notifications at the right scrollTop
diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js
index 5a05de28e..7ab12cf53 100644
--- a/client/coral-framework/reducers/auth.js
+++ b/client/coral-framework/reducers/auth.js
@@ -151,6 +151,20 @@ export default function auth (state = initialState, action) {
case actions.SET_REDIRECT_URI:
return state
.set('redirectUri', action.uri);
+ case 'APOLLO_SUBSCRIPTION_RESULT':
+ if (action.operationName === 'UserBanned' && state.getIn(['user', 'id']) === action.variables.user_id) {
+ return state
+ .mergeIn(['user'], action.result.data.userBanned);
+ }
+ if (action.operationName === 'UserSuspended' && state.getIn(['user', 'id']) === action.variables.user_id) {
+ return state
+ .mergeIn(['user'], action.result.data.userSuspended);
+ }
+ if (action.operationName === 'UsernameRejected' && state.getIn(['user', 'id']) === action.variables.user_id) {
+ return state
+ .mergeIn(['user'], action.result.data.usernameRejected);
+ }
+ return state;
default :
return state;
}
diff --git a/client/coral-plugin-author-name/AuthorName.js b/client/coral-plugin-author-name/AuthorName.js
index 66b907ad8..7d453d96f 100644
--- a/client/coral-plugin-author-name/AuthorName.js
+++ b/client/coral-plugin-author-name/AuthorName.js
@@ -24,7 +24,7 @@ export default class AuthorName extends Component {
return (
- {author && author.name}
+ {author && author.username}
);
}
diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js
index 5105667ed..efa1d27ed 100644
--- a/client/coral-plugin-commentbox/CommentBox.js
+++ b/client/coral-plugin-commentbox/CommentBox.js
@@ -1,6 +1,7 @@
import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
+import {can} from 'coral-framework/services/perms';
import Slot from 'coral-framework/components/Slot';
import {connect} from 'react-redux';
@@ -43,8 +44,14 @@ class CommentBox extends React.Component {
assetId,
parentId,
addNotification,
+ currentUser,
} = this.props;
+ if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) {
+ addNotification('error', t('error.NOT_AUTHORIZED'));
+ return;
+ }
+
let comment = {
asset_id: assetId,
parent_id: parentId,
@@ -126,7 +133,7 @@ class CommentBox extends React.Component {
handleChange = (e) => this.setState({body: e.target.value});
render () {
- const {styles, isReply, authorId, maxCharCount} = this.props;
+ const {styles, isReply, currentUser, maxCharCount} = this.props;
let {cancelButtonClicked} = this.props;
if (isReply && typeof cancelButtonClicked !== 'function') {
@@ -145,7 +152,7 @@ class CommentBox extends React.Component {
charCountEnable={this.props.charCountEnable}
bodyPlaceholder={t('comment.comment')}
bodyInputId={isReply ? 'replyText' : 'commentText'}
- saveComment={authorId && this.postComment}
+ saveComment={currentUser && this.postComment}
buttonContainerStart={
;
@@ -20,17 +21,17 @@ const getPopupMenu = [
(itemType) => {
const options = itemType === 'COMMENTS' ?
[
- {val: 'This comment is offensive', text: t('comment_offensive')},
- {val: 'This looks like an ad/marketing', text: t('marketing')},
- {val: 'I don\'t agree with this comment', text: t('no_agree_comment')},
- {val: 'Other', text: t('other')}
+ {val: comment.offensive, text: t('comment_offensive')},
+ {val: comment.spam, text: t('marketing')},
+ {val: comment.noagree, text: t('no_agree_comment')},
+ {val: comment.other, text: t('other')}
]
: [
- {val: 'This username is offensive', text: t('username_offensive')},
- {val: 'I don\'t like this username', text: t('no_like_username')},
- {val: 'This user is impersonating', text: t('user_impersonating')},
- {val: 'This looks like an ad/marketing', text: t('marketing')},
- {val: 'Other', text: t('other')}
+ {val: username.offensive, text: t('username_offensive')},
+ {val: username.nolike, text: t('no_like_username')},
+ {val: username.impersonating, text: t('user_impersonating')},
+ {val: username.spam, text: t('marketing')},
+ {val: username.other, text: t('other')}
];
return {
header: t('step_2_header'),
diff --git a/client/coral-plugin-flags/helpers/flagReasons.js b/client/coral-plugin-flags/helpers/flagReasons.js
new file mode 100644
index 000000000..514c57398
--- /dev/null
+++ b/client/coral-plugin-flags/helpers/flagReasons.js
@@ -0,0 +1,15 @@
+export default {
+ username: {
+ offensive: 'USERNAME_OFFENSIVE',
+ nolike: 'USERNAME_NOLIKE',
+ impersonating: 'USERNAME_IMPERSONATING',
+ spam: 'USERNAME_SPAM',
+ other: 'USERNAME_OTHER'
+ },
+ comment: {
+ offensive: 'COMMENT_OFFENSIVE',
+ spam: 'COMMENT_SPAM',
+ noagree: 'COMMENT_NOAGREE',
+ other: 'COMMENT_OTHER'
+ }
+};
diff --git a/client/coral-plugin-flags/index.js b/client/coral-plugin-flags/index.js
new file mode 100644
index 000000000..d896d34f5
--- /dev/null
+++ b/client/coral-plugin-flags/index.js
@@ -0,0 +1 @@
+export {default as FlagComment} from './components/FlagComment';
\ No newline at end of file
diff --git a/client/coral-plugin-permalinks/PermalinkButton.js b/client/coral-plugin-permalinks/PermalinkButton.js
deleted file mode 100644
index 3d6df996f..000000000
--- a/client/coral-plugin-permalinks/PermalinkButton.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import React, {PropTypes} from 'react';
-const name = 'coral-plugin-permalinks';
-import {Button} from 'coral-ui';
-import styles from './styles.css';
-import ClickOutside from 'coral-framework/components/ClickOutside';
-
-import t from 'coral-framework/services/i18n';
-
-export default class PermalinkButton extends React.Component {
-
- static propTypes = {
- articleURL: PropTypes.string.isRequired,
- commentId: PropTypes.string.isRequired
- }
-
- constructor (props) {
- super(props);
- this.state = {popoverOpen: false, copySuccessful: null, copyFailure: null};
- this.toggle = this.toggle.bind(this);
- this.copyPermalink = this.copyPermalink.bind(this);
- }
-
- toggle () {
-
- // I wish I could position this with a stylesheet, but top-level comments with
- // nested replies throws everything off, as well as very long comments
- this.popover.style.top = `${this.linkButton.offsetTop - 80}px`;
- this.setState({popoverOpen: !this.state.popoverOpen});
- }
-
- handleClickOutside = () => {
- this.setState({popoverOpen: false});
- }
-
- copyPermalink () {
- this.permalinkInput.select();
- try {
- document.execCommand('copy');
- this.setState({copySuccessful: true, copyFailure: null});
- } catch (err) {
- this.setState({copyFailure: true, copySuccessful: null});
- }
-
- setTimeout(() => {
- this.setState({copyFailure: null, copySuccessful: null});
- }, 3000);
- }
-
- render () {
- const {copySuccessful, copyFailure} = this.state;
- return (
-
-
-
-
this.popover = ref}
- className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
- this.permalinkInput = input}
- value={`${this.props.articleURL}#${this.props.commentId}`}
- onChange={() => {}} />
-
-
-
-
- );
- }
-}
diff --git a/client/coral-plugin-permalinks/styles.css b/client/coral-plugin-permalinks/styles.css
deleted file mode 100644
index a4671c685..000000000
--- a/client/coral-plugin-permalinks/styles.css
+++ /dev/null
@@ -1,67 +0,0 @@
-.container {
- border-radius: 3px;
- padding: 15px 10px;
- box-sizing: border-box;
- /* box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3); */
- border: solid 1px rgba(153, 153, 153, 0.33);
- left: 0;
- width: 100%;
-
- &::before {
- content: '';
- border: 10px solid transparent;
- border-top-color: white;
- position: absolute;
- right: 7em;
- bottom: -20px;
- z-index: 2;
- }
-
- &::after{
- content: '';
- border: 10px solid transparent;
- border-top-color: rgba(153, 153, 153, 0.33);
- position: absolute;
- right: 7em;
- bottom: -21px;
- z-index: 1;
- }
-
- input {
- display: inline-block;
- width: calc(100% - 78px);
- padding: 8px;
- border-radius: 3px;
- border: solid 1px #e0e0e0;
- height: 32px;
- box-sizing: border-box;
- font-size: 1em;
- }
-
- button {
- display: inline-block;
- float: right;
- box-sizing: border-box;
- margin: 0;
- background-color: #e0e0e0;
- font-size: 1em;
- width: auto;
- height: auto;
- padding: 2px;
- transition: background-color 0.4s ease;
-
- &:hover{
- color: black;
- }
-
- &.success {
- background-color: #00897B;
- color: white;
- }
-
- &.failure {
- background-color: #FF5252;
- color: white;
- }
- }
-}
diff --git a/client/coral-plugin-replies/ReplyBox.js b/client/coral-plugin-replies/ReplyBox.js
index 8241bf511..cc8e8045f 100644
--- a/client/coral-plugin-replies/ReplyBox.js
+++ b/client/coral-plugin-replies/ReplyBox.js
@@ -18,7 +18,7 @@ class ReplyBox extends Component {
styles,
postComment,
assetId,
- authorId,
+ currentUser,
addNotification,
parentId,
commentPostedHandler,
@@ -33,7 +33,7 @@ class ReplyBox extends Component {
parentId={parentId}
cancelButtonClicked={this.cancelReply}
addNotification={addNotification}
- authorId={authorId}
+ currentUser={currentUser}
assetId={assetId}
postComment={postComment}
isReply={true} />
@@ -48,7 +48,6 @@ ReplyBox.propTypes = {
commentPostedHandler: PropTypes.func,
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
- authorId: PropTypes.string.isRequired,
postComment: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired
};
diff --git a/graph/context.js b/graph/context.js
index 66086dc89..d2306cbc8 100644
--- a/graph/context.js
+++ b/graph/context.js
@@ -3,6 +3,7 @@ const mutators = require('./mutators');
const uuid = require('uuid');
const plugins = require('../services/plugins');
+const pubsub = require('../services/pubsub');
const debug = require('debug')('talk:graph:context');
/**
@@ -32,7 +33,7 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc
* Stores the request context.
*/
class Context {
- constructor({user = null}, pubsub) {
+ constructor({user = null}) {
// Generate a new context id for the request.
this.id = uuid.v4();
diff --git a/graph/index.js b/graph/index.js
index 4e99714d9..7495c0605 100644
--- a/graph/index.js
+++ b/graph/index.js
@@ -1,6 +1,5 @@
const schema = require('./schema');
const Context = require('./context');
-const pubsub = require('./pubsub');
const {createSubscriptionManager} = require('./subscriptions');
module.exports = {
@@ -11,7 +10,7 @@ module.exports = {
// Load in the new context here, this'll create the loaders + mutators for
// the lifespan of this request.
- context: new Context(req, pubsub)
+ context: new Context(req)
}),
createSubscriptionManager
};
diff --git a/graph/mutators/action.js b/graph/mutators/action.js
index 19db33183..d62d3b4c5 100644
--- a/graph/mutators/action.js
+++ b/graph/mutators/action.js
@@ -16,7 +16,7 @@ const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
let comment;
- if (pubsub && item_type === 'COMMENTS') {
+ if (item_type === 'COMMENTS') {
comment = await Comments.get.load(item_id);
if (!comment) {
throw new Error('Comment not found');
@@ -38,7 +38,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
await UsersService.setStatus(item_id, 'PENDING');
}
- if (pubsub && comment) {
+ if (comment) {
pubsub.publish('commentFlagged', comment);
}
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index df6ed71ce..f32172a4d 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -177,11 +177,8 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
}
Comments.countByAssetID.incr(asset_id);
- if (pubsub) {
-
- // Publish the newly added comment via the subscription.
- pubsub.publish('commentAdded', comment);
- }
+ // Publish the newly added comment via the subscription.
+ pubsub.publish('commentAdded', comment);
}
return comment;
@@ -346,17 +343,14 @@ 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 (pubsub) {
+ if (status === 'ACCEPTED') {
- 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('commentAccepted', comment);
- } else if (status === 'REJECTED') {
-
- // Publish the comment status change via the subscription.
- pubsub.publish('commentRejected', comment);
- }
+ // Publish the comment status change via the subscription.
+ pubsub.publish('commentRejected', comment);
}
return comment;
@@ -379,11 +373,9 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
// Execute the edit.
const comment = await CommentsService.edit(id, context.user.id, {body, status});
- if (context.pubsub) {
+ // Publish the edited comment via the subscription.
+ context.pubsub.publish('commentEdited', comment);
- // Publish the edited comment via the subscription.
- context.pubsub.publish('commentEdited', comment);
- }
return comment;
};
diff --git a/graph/mutators/user.js b/graph/mutators/user.js
index cf0106a2c..448ac750a 100644
--- a/graph/mutators/user.js
+++ b/graph/mutators/user.js
@@ -2,16 +2,28 @@ const errors = require('../../errors');
const UsersService = require('../../services/users');
const {SET_USER_STATUS, SUSPEND_USER, REJECT_USERNAME} = require('../../perms/constants');
-const setUserStatus = ({user}, {id, status}) => {
- return UsersService.setStatus(id, status);
+const setUserStatus = async ({user, pubsub}, {id, status}) => {
+ const result = await UsersService.setStatus(id, status);
+ if (result && result.status === 'BANNED') {
+ pubsub.publish('userBanned', result);
+ }
+ return result;
};
-const suspendUser = ({user}, {id, message, until}) => {
- return UsersService.suspendUser(id, message, until);
+const suspendUser = async ({user, pubsub}, {id, message, until}) => {
+ const result = await UsersService.suspendUser(id, message, until);
+ if (result) {
+ pubsub.publish('userSuspended', result);
+ }
+ return result;
};
-const rejectUsername = ({user}, {id, message}) => {
- return UsersService.rejectUsername(id, message);
+const rejectUsername = async ({user, pubsub}, {id, message}) => {
+ const result = await UsersService.rejectUsername(id, message);
+ if (result) {
+ pubsub.publish('usernameRejected', result);
+ }
+ return result;
};
const ignoreUser = ({user}, userToIgnore) => {
diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js
index 1f26d5ff3..f6da9fb54 100644
--- a/graph/resolvers/subscription.js
+++ b/graph/resolvers/subscription.js
@@ -14,6 +14,15 @@ const Subscription = {
commentFlagged(comment) {
return comment;
},
+ userBanned(user) {
+ return user;
+ },
+ userSuspended(user) {
+ return user;
+ },
+ usernameRejected(user) {
+ return user;
+ },
};
module.exports = Subscription;
diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js
index 035af22cb..2878d6561 100644
--- a/graph/resolvers/user.js
+++ b/graph/resolvers/user.js
@@ -6,6 +6,7 @@ const {
SEARCH_OTHERS_COMMENTS,
UPDATE_USER_ROLES,
SEARCH_COMMENT_METRICS,
+ VIEW_SUSPENSION_INFO,
LIST_OWN_TOKENS
} = require('../../perms/constants');
@@ -84,6 +85,13 @@ const User = {
if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) {
return KarmaService.model(user);
}
+ },
+
+ suspension({id, suspension}, _, {user}) {
+ if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) {
+ return null;
+ }
+ return suspension;
}
};
diff --git a/graph/subscriptions.js b/graph/subscriptions.js
index ec2d5b786..ce1eb1a09 100644
--- a/graph/subscriptions.js
+++ b/graph/subscriptions.js
@@ -3,7 +3,7 @@ const {SubscriptionServer} = require('subscriptions-transport-ws');
const _ = require('lodash');
const debug = require('debug')('talk:graph:subscriptions');
-const pubsub = require('./pubsub');
+const pubsub = require('../services/pubsub');
const schema = require('./schema');
const Context = require('./context');
const plugins = require('../services/plugins');
@@ -21,6 +21,9 @@ const {
SUBSCRIBE_COMMENT_FLAGGED,
SUBSCRIBE_ALL_COMMENT_EDITED,
SUBSCRIBE_ALL_COMMENT_ADDED,
+ SUBSCRIBE_ALL_USER_SUSPENDED,
+ SUBSCRIBE_ALL_USER_BANNED,
+ SUBSCRIBE_ALL_USERNAME_REJECTED,
} = require('../perms/constants');
/**
@@ -83,6 +86,45 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
}
},
}),
+ userSuspended: (options, args) => ({
+ userSuspended: {
+ filter: (user, context) => {
+ if (
+ !context.user
+ || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED)
+ ) {
+ return false;
+ }
+ return !args.user_id || user.id === args.user_id;
+ }
+ },
+ }),
+ userBanned: (options, args) => ({
+ userBanned: {
+ filter: (user, context) => {
+ if (
+ !context.user
+ || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED)
+ ) {
+ return false;
+ }
+ return !args.user_id || user.id === args.user_id;
+ }
+ },
+ }),
+ usernameRejected: (options, args) => ({
+ usernameRejected: {
+ filter: (user, context) => {
+ if (
+ !context.user
+ || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED)
+ ) {
+ return false;
+ }
+ return !args.user_id || user.id === args.user_id;
+ }
+ },
+ }),
});
/**
@@ -117,10 +159,10 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
} catch (e) {
console.error(e);
- return new Context({}, pubsub);
+ return new Context({});
}
- return new Context(req, pubsub);
+ return new Context(req);
};
return baseParams;
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 7801872a6..4a38ce266 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -61,6 +61,10 @@ type UserProfile {
provider: String!
}
+type SuspensionInfo {
+ until: Date
+}
+
# Any person who can author comments, create actions, and view comments on a
# stream.
type User {
@@ -108,6 +112,10 @@ type User {
# returns user status
status: USER_STATUS
+
+ # returns suspension info. Only available to Admins and Moderators
+ # or on own logged in User.
+ suspension: SuspensionInfo
}
# UsersQuery allows the ability to query users by a specific fields.
@@ -1034,6 +1042,21 @@ type Subscription {
# Get an update whenever a comment has been rejected.
# Requires the `ADMIN` or `MODERATOR` role.
commentRejected(asset_id: ID): Comment
+
+ # Get an update whenever a user has been suspended.
+ # `user_id` must match id of current user except for
+ # users with the `ADMIN` or `MODERATOR` role.
+ userSuspended(user_id: ID): User
+
+ # Get an update whenever a user has been banned.
+ # `user_id` must match id of current user except for
+ # users with the `ADMIN` or `MODERATOR` role.
+ userBanned(user_id: ID): User
+
+ # Get an update whenever a username has been rejected.
+ # `user_id` must match id of current user except for
+ # users with the `ADMIN` or `MODERATOR` role.
+ usernameRejected(user_id: ID): User
}
################################################################################
diff --git a/locales/en.yml b/locales/en.yml
index 1fac86785..03256e75e 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -1,4 +1,7 @@
en:
+ your_account_has_been_suspended: Your account has been temporarily suspended.
+ your_account_has_been_banned: Your account has been banned.
+ your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.
bandialog:
are_you_sure: "Are you sure you would like to ban {0}?"
ban_user: "Ban User?"
diff --git a/locales/es.yml b/locales/es.yml
index 62ea0e97e..7297fedd5 100644
--- a/locales/es.yml
+++ b/locales/es.yml
@@ -1,4 +1,7 @@
es:
+ your_account_has_been_suspended: Su cuenta ha sido temporalmente suspendida.
+ your_account_has_been_banned: Su cuenta ha sido suspendida.
+ your_username_has_been_rejected: Su cuenta ha sido suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario.
bandialog:
are_you_sure: "¿Estás segura que quieres suspender a {0}?"
ban_user: "¿Quieres suspender el Usuario?"
diff --git a/package.json b/package.json
index 729358ffc..2834a3e3a 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
"passport-local": "^1.0.0",
"prop-types": "^15.5.10",
"react-apollo": "^1.1.0",
+ "react-input-autosize": "^1.1.4",
"react-recaptcha": "^2.2.6",
"react-toastify": "^1.5.0",
"react-transition-group": "^1.1.3",
@@ -117,6 +118,7 @@
"resolve": "^1.3.2",
"semver": "^5.3.0",
"simplemde": "^1.11.2",
+ "smoothscroll-polyfill": "^0.3.5",
"snake-case": "^2.1.0",
"subscriptions-transport-ws": "^0.7.2",
"timekeeper": "^1.0.0",
@@ -195,7 +197,7 @@
"react-mdl-selectfield": "^0.2.0",
"react-redux": "^4.4.5",
"react-router": "^3.0.0",
- "react-tagsinput": "^3.14.0",
+ "react-tagsinput": "^3.17.0",
"redux": "^3.6.0",
"redux-mock-store": "^1.2.1",
"redux-thunk": "^2.1.0",
diff --git a/perms/constants.js b/perms/constants.js
index abfd16c6f..383f36daf 100644
--- a/perms/constants.js
+++ b/perms/constants.js
@@ -26,6 +26,7 @@ module.exports = {
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
+ VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
// subscriptions
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
@@ -33,4 +34,7 @@ module.exports = {
SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED',
SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED',
SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED',
+ SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED',
+ SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED',
+ SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED',
};
diff --git a/perms/queryReducer.js b/perms/queryReducer.js
index ed84f2265..0b76c225a 100644
--- a/perms/queryReducer.js
+++ b/perms/queryReducer.js
@@ -19,6 +19,8 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN']);
case types.SEARCH_COMMENT_STATUS_HISTORY:
return check(user, ['ADMIN', 'MODERATOR']);
+ case types.VIEW_SUSPENSION_INFO:
+ return check(user, ['ADMIN', 'MODERATOR']);
default:
break;
}
diff --git a/perms/subscriptionReducer.js b/perms/subscriptionReducer.js
index 0b06902cf..e0f73526a 100644
--- a/perms/subscriptionReducer.js
+++ b/perms/subscriptionReducer.js
@@ -13,6 +13,12 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_COMMENT_ADDED:
return check(user, ['ADMIN', 'MODERATOR']);
+ case types.SUBSCRIBE_ALL_USER_SUSPENDED:
+ return check(user, ['ADMIN', 'MODERATOR']);
+ case types.SUBSCRIBE_ALL_USER_BANNED:
+ return check(user, ['ADMIN', 'MODERATOR']);
+ case types.SUBSCRIBE_ALL_USERNAME_REJECTED:
+ return check(user, ['ADMIN', 'MODERATOR']);
default:
break;
}
diff --git a/plugin-api/beta/client/components/index.js b/plugin-api/beta/client/components/index.js
index 5d76732fc..6466b9ffc 100644
--- a/plugin-api/beta/client/components/index.js
+++ b/plugin-api/beta/client/components/index.js
@@ -1 +1,2 @@
export {Slot} from 'coral-framework/components';
+export {default as ClickOutside} from 'coral-framework/components/ClickOutside';
diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js
index 1ed777996..72de7f5c4 100644
--- a/plugin-api/beta/client/hocs/withReaction.js
+++ b/plugin-api/beta/client/hocs/withReaction.js
@@ -8,6 +8,7 @@ import {compose, gql} from 'react-apollo';
import withFragments from 'coral-framework/hocs/withFragments';
import withMutation from 'coral-framework/hocs/withMutation';
import {showSignInDialog} from 'coral-framework/actions/auth';
+import {addNotification} from 'coral-framework/actions/notification';
import {capitalize} from 'coral-framework/helpers/strings';
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
import * as PropTypes from 'prop-types';
@@ -248,6 +249,7 @@ export default (reaction) => (WrappedComponent) => {
return (WrappedComponent) => {
});
const mapDispatchToProps = (dispatch) =>
- bindActionCreators({showSignInDialog}, dispatch);
+ bindActionCreators({showSignInDialog, addNotification}, dispatch);
const enhance = compose(
withFragments({
diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js
index 68a516c3f..7d17ef056 100644
--- a/plugins/coral-plugin-like/client/LikeButton.js
+++ b/plugins/coral-plugin-like/client/LikeButton.js
@@ -13,6 +13,7 @@ class LikeButton extends React.Component {
postReaction,
deleteReaction,
showSignInDialog,
+ addNotification,
alreadyReacted,
user,
} = this.props;
@@ -25,6 +26,7 @@ class LikeButton extends React.Component {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
+ addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js
index 0fc7bd7ca..7450ad668 100644
--- a/plugins/coral-plugin-love/client/LoveButton.js
+++ b/plugins/coral-plugin-love/client/LoveButton.js
@@ -13,6 +13,7 @@ class LoveButton extends React.Component {
postReaction,
deleteReaction,
showSignInDialog,
+ addNotification,
alreadyReacted,
user,
} = this.props;
@@ -25,6 +26,7 @@ class LoveButton extends React.Component {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
+ addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js
index bde8c9669..9bc556b67 100644
--- a/plugins/coral-plugin-respect/client/RespectButton.js
+++ b/plugins/coral-plugin-respect/client/RespectButton.js
@@ -14,6 +14,7 @@ class RespectButton extends React.Component {
deleteReaction,
showSignInDialog,
alreadyReacted,
+ addNotification,
user,
} = this.props;
@@ -25,6 +26,7 @@ class RespectButton extends React.Component {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
+ addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
diff --git a/plugins/talk-plugin-permalink/client/.babelrc b/plugins/talk-plugin-permalink/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/talk-plugin-permalink/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-permalink/client/.eslintrc.json b/plugins/talk-plugin-permalink/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/talk-plugin-permalink/client/.eslintrc.json
@@ -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"] }]
+ }
+}
diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js
new file mode 100644
index 000000000..68406472e
--- /dev/null
+++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js
@@ -0,0 +1,107 @@
+import React from 'react';
+import cn from 'classnames';
+import styles from './styles.css';
+import {t} from 'plugin-api/beta/client/services';
+import {ClickOutside} from 'plugin-api/beta/client/components';
+import {Icon, Button} from 'plugin-api/beta/client/components/ui';
+
+const name = 'talk-plugin-permalink';
+
+export default class PermalinkButton extends React.Component {
+ constructor (props) {
+ super(props);
+
+ this.state = {
+ popoverOpen: false,
+ copySuccessful: null,
+ copyFailure: null
+ };
+
+ }
+
+ toggle = () => {
+ this.popover.style.top = `${this.linkButton.offsetTop - 80}px`;
+
+ this.setState({
+ popoverOpen: !this.state.popoverOpen
+ });
+ }
+
+ handleClickOutside = () => {
+ this.setState({
+ popoverOpen: false
+ });
+ }
+
+ copyPermalink = () => {
+ this.permalinkInput.select();
+ try {
+ document.execCommand('copy');
+ this.setState({
+ copySuccessful: true,
+ copyFailure: null
+ });
+ } catch (err) {
+ this.setState({
+ copyFailure: true,
+ copySuccessful: null
+ });
+ }
+
+ this.timeout = window.setTimeout(() => {
+ this.setState({
+ copyFailure: null,
+ copySuccessful: null
+ });
+ }, 3000);
+ }
+
+ componentWillUnmount() {
+ window.clearTimeout(this.timeout);
+ }
+
+ render () {
+ const {copySuccessful, copyFailure, popoverOpen} = this.state;
+ const {asset} = this.props;
+ return (
+
+
+
+
+
+
this.popover = ref}
+ className={cn([`${name}-popover`, styles.popover, {[styles.active]: popoverOpen}])}>
+
+ this.permalinkInput = input}
+ defaultValue={`${asset.url}#${this.props.commentId}`}
+ />
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/plugins/talk-plugin-permalink/client/components/styles.css b/plugins/talk-plugin-permalink/client/components/styles.css
new file mode 100644
index 000000000..56d069755
--- /dev/null
+++ b/plugins/talk-plugin-permalink/client/components/styles.css
@@ -0,0 +1,77 @@
+.popover {
+ display: none;
+ background-color: white;
+ position: absolute;
+ border-radius: 3px;
+ padding: 15px 10px;
+ box-sizing: border-box;
+ border: solid 1px rgba(153, 153, 153, 0.33);
+ left: 0;
+ width: 100%;
+ z-index: 10;
+}
+.popover::before {
+ content: '';
+ border: 10px solid transparent;
+ border-top-color: white;
+ position: absolute;
+ right: 7em;
+ bottom: -20px;
+ z-index: 2;
+}
+
+.popover::after{
+ content: '';
+ border: 10px solid transparent;
+ border-top-color: rgba(153, 153, 153, 0.33);
+ position: absolute;
+ right: 7em;
+ bottom: -21px;
+ z-index: 1;
+}
+
+.input {
+ display: inline-block;
+ width: calc(100% - 78px);
+ padding: 8px;
+ border-radius: 3px;
+ border: solid 1px #e0e0e0;
+ height: 32px;
+ box-sizing: border-box;
+ font-size: 1em;
+}
+
+.button {
+ display: inline-block;
+ float: right;
+ box-sizing: border-box;
+ margin: 0;
+ background-color: #e0e0e0;
+ font-size: 1em;
+ width: auto;
+ height: auto;
+ padding: 2px;
+ transition: background-color 0.4s ease;
+}
+
+.button:hover{
+ color: black;
+}
+
+.button.success {
+ background-color: #00897B;
+ color: white;
+}
+
+.button.failure {
+ background-color: #FF5252;
+ color: white;
+}
+
+.container {
+ z-index: 2;
+}
+
+.active {
+ display: block;
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-permalink/client/index.js b/plugins/talk-plugin-permalink/client/index.js
new file mode 100644
index 000000000..d413da870
--- /dev/null
+++ b/plugins/talk-plugin-permalink/client/index.js
@@ -0,0 +1,7 @@
+import PermalinkButton from './components/PermalinkButton';
+
+export default {
+ slots: {
+ commentActions: [PermalinkButton]
+ }
+};
diff --git a/plugins/talk-plugin-permalink/index.js b/plugins/talk-plugin-permalink/index.js
new file mode 100644
index 000000000..f053ebf79
--- /dev/null
+++ b/plugins/talk-plugin-permalink/index.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/plugins/talk-plugin-permalink/package.json b/plugins/talk-plugin-permalink/package.json
new file mode 100644
index 000000000..fefeb9f16
--- /dev/null
+++ b/plugins/talk-plugin-permalink/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "talk-plugin-permalink",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "author": "",
+ "license": "ISC"
+}
diff --git a/routes/api/users/index.js b/routes/api/users/index.js
index 10d72fe3a..47ff46853 100644
--- a/routes/api/users/index.js
+++ b/routes/api/users/index.js
@@ -1,8 +1,8 @@
const express = require('express');
const router = express.Router();
const UsersService = require('../../../services/users');
-const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
+const pubsub = require('../../../services/pubsub');
const errors = require('../../../errors');
const authorization = require('../../../middleware/authorization');
const i18n = require('../../../services/i18n');
@@ -53,11 +53,16 @@ router.post('/:user_id/role', authorization.needed('ADMIN'), (req, res, next) =>
router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) => {
UsersService
.setStatus(req.params.user_id, req.body.status)
- .then((status) => {
- res.status(201).json(status);
+ .then((user) => {
- if (status === 'BANNED' && req.body.comment_id) {
- return CommentsService.pushStatus(req.body.comment_id, 'rejected', req.params.user_id);
+ // TODO: current updating status behavior is weird.
+ if (user) {
+ if (user.status === 'BANNED') {
+ pubsub.publish('userBanned', user);
+ }
+ res.status(201).json(user.status);
+ } else {
+ res.status(500).json();
}
})
.catch(next);
diff --git a/graph/pubsub.js b/services/pubsub.js
similarity index 69%
rename from graph/pubsub.js
rename to services/pubsub.js
index 704f83725..a42ffb314 100644
--- a/graph/pubsub.js
+++ b/services/pubsub.js
@@ -1,5 +1,5 @@
const {RedisPubSub} = require('graphql-redis-subscriptions');
-const {connectionOptions} = require('../services/redis');
+const {connectionOptions} = require('./redis');
module.exports = new RedisPubSub({connection: connectionOptions});
diff --git a/services/users.js b/services/users.js
index 78ec57b4c..13dddaf04 100644
--- a/services/users.js
+++ b/services/users.js
@@ -435,7 +435,10 @@ module.exports = class UsersService {
return Promise.reject(new Error(`status ${status} is not supported`));
}
- return UserModel.update({
+ // TODO: current updating status behavior is weird.
+ // once a user has been `APPROVED` its status cannot be
+ // changed anymore.
+ return UserModel.findOneAndUpdate({
id,
status: {
$ne: 'APPROVED'
@@ -444,6 +447,8 @@ module.exports = class UsersService {
$set: {
status
}
+ }, {
+ new: true,
});
}
@@ -453,34 +458,37 @@ module.exports = class UsersService {
* @param {String} message message to be send to the user
* @param {Date} until date until the suspension is valid.
*/
- static suspendUser(id, message, until) {
- return UserModel.findOneAndUpdate(
+ static async suspendUser(id, message, until) {
+ const user = await UserModel.findOneAndUpdate(
{id}, {
$set: {
suspension: {
until,
},
}
- })
- .then((user) => {
- if (message) {
- let localProfile = user.profiles.find((profile) => profile.provider === 'local');
- if (localProfile) {
- const options =
- {
- template: 'suspension', // needed to know which template to render!
- locals: { // specifies the template locals.
- body: message
- },
- subject: 'Your account has been suspended',
- to: localProfile.id // This only works if the user has registered via e-mail.
- // We may want a standard way to access a user's e-mail address in the future
- };
-
- return MailerService.sendSimple(options);
- }
- }
+ }, {
+ new: true,
});
+
+ if (message) {
+ let localProfile = user.profiles.find((profile) => profile.provider === 'local');
+ if (localProfile) {
+ const options =
+ {
+ template: 'suspension', // needed to know which template to render!
+ locals: { // specifies the template locals.
+ body: message
+ },
+ subject: 'Your account has been suspended',
+ to: localProfile.id // This only works if the user has registered via e-mail.
+ // We may want a standard way to access a user's e-mail address in the future
+ };
+
+ await MailerService.sendSimple(options);
+ }
+ }
+
+ return user;
}
/**
@@ -489,34 +497,37 @@ module.exports = class UsersService {
* @param {String} message message to be send to the user
* @param {Date} until date until the suspension is valid.
*/
- static rejectUsername(id, message) {
- return UserModel.findOneAndUpdate({
+ static async rejectUsername(id, message) {
+ const user = await UserModel.findOneAndUpdate({
id
}, {
$set: {
status: 'BANNED',
canEditName: true,
}
- })
- .then((user) => {
- if (message) {
- let localProfile = user.profiles.find((profile) => profile.provider === 'local');
- if (localProfile) {
- const options =
- {
- template: 'suspension', // needed to know which template to render!
- locals: { // specifies the template locals.
- body: message
- },
- subject: 'Email Suspension',
- to: localProfile.id // This only works if the user has registered via e-mail.
- // We may want a standard way to access a user's e-mail address in the future
- };
-
- return MailerService.sendSimple(options);
- }
- }
+ }, {
+ new: true,
});
+
+ if (message) {
+ let localProfile = user.profiles.find((profile) => profile.provider === 'local');
+ if (localProfile) {
+ const options =
+ {
+ template: 'suspension', // needed to know which template to render!
+ locals: { // specifies the template locals.
+ body: message
+ },
+ subject: 'Email Suspension',
+ to: localProfile.id // This only works if the user has registered via e-mail.
+ // We may want a standard way to access a user's e-mail address in the future
+ };
+
+ await MailerService.sendSimple(options);
+ }
+ }
+
+ return user;
}
/**
@@ -863,11 +874,24 @@ module.exports = class UsersService {
* @return {Promise}
*/
static async editName(id, username) {
+
+ // TODO: Revisit this when we revamped User status workflows.
+ const queryUsernameRejected = {
+ id,
+ username: {$ne: username},
+ status: 'BANNED',
+ canEditName: true
+ };
+
+ const queryCreateUsername = {
+ id,
+ status: 'ACTIVE',
+ canEditName: true
+ };
+
try {
const result = await UserModel.findOneAndUpdate({
- id,
- username: {$ne: username},
- canEditName: true
+ $or: [queryUsernameRejected, queryCreateUsername],
}, {
$set: {
username: username,
diff --git a/test/server/graph/context.js b/test/server/graph/context.js
index b01917289..7d30042e4 100644
--- a/test/server/graph/context.js
+++ b/test/server/graph/context.js
@@ -3,14 +3,16 @@ const expect = require('chai').expect;
const User = require('../../../models/user');
const Context = require('../../../graph/context');
const errors = require('../../../errors');
+const SettingsService = require('../../../services/settings');
describe('graph.Context', () => {
+ beforeEach(() => SettingsService.init());
describe('#constructor: with a user', () => {
let c;
beforeEach(() => {
- c = new Context({user: new User({id: '1'})});
+ c = new Context({user: new User({id: '1', roles: ['ADMIN']})});
});
it('creates a context with a user', (done) => {
@@ -21,15 +23,10 @@ describe('graph.Context', () => {
});
it('does have access to mutators', () => {
- return c.mutators.Action.create({
- item_id: '1',
- item_type: 'COMMENTS',
- action_type: 'LIKE'
- })
- .then((action) => {
- expect(action).to.have.property('item_id', '1');
- expect(action).to.have.property('item_type', 'COMMENTS');
- expect(action).to.have.property('action_type', 'LIKE');
+ return c.mutators.Tag.add({
+ item_type: 'USERS',
+ id: '1',
+ name: 'Tag',
});
});
});
@@ -48,13 +45,13 @@ describe('graph.Context', () => {
});
it('does not have access to mutators', () => {
- return c.mutators.Action.create({
- item_id: '1',
+ return c.mutators.Tag.add({
item_type: 'COMMENTS',
- action_type: 'LIKE'
+ id: '1',
+ name: 'Tag',
})
- .then((action) => {
- expect(action).to.be.null;
+ .then(() => {
+ throw new Error('should not reach this point');
})
.catch((err) => {
expect(err).to.be.equal(errors.ErrNotAuthorized);
diff --git a/test/server/routes/api/account/index.js b/test/server/routes/api/account/index.js
index 08b9c8b1d..8d4e0057a 100644
--- a/test/server/routes/api/account/index.js
+++ b/test/server/routes/api/account/index.js
@@ -54,23 +54,6 @@ describe('/api/v1/account/username', () => {
});
});
- it('it should return an error when the user submits the same username', (done) => {
- chai.request(app)
- .post(`/api/v1/users/${mockUser.id}/username-enable`)
- .set(passport.inject({id: '456', roles: ['ADMIN']}))
- .then(() => chai.request(app)
- .put('/api/v1/account/username')
- .set(passport.inject({id: mockUser.id, roles: []}))
- .send({username: 'Ana'}))
- .then(() => {
- done(new Error('Expected Error'));
- })
- .catch((err) => {
- expect(err).to.be.ok;
- done();
- });
- });
-
it('it should return an error when the user tries to edit their username if canEditName is disabled', (done) => {
chai.request(app)
.put('/api/v1/account/username')
diff --git a/test/server/services/users.js b/test/server/services/users.js
index 027ac524c..53f4bc018 100644
--- a/test/server/services/users.js
+++ b/test/server/services/users.js
@@ -238,6 +238,32 @@ describe('services.UsersService', () => {
});
});
+ it('should let the user submit the same username if user is not banned (create username)', () => {
+ return UsersService
+ .toggleNameEdit(mockUsers[0].id, true)
+ .then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
+ .then(() => UsersService.findById(mockUsers[0].id))
+ .then((user) => {
+ expect(user).to.have.property('username', mockUsers[0].username);
+ expect(user).to.have.property('canEditName', false);
+ });
+ });
+
+ it('should return error when a banned user submits the same username (rejected username)', () => {
+ return UsersService
+ .toggleNameEdit(mockUsers[0].id, true)
+ .then(() => UsersService.setStatus(mockUsers[0].id, 'BANNED'))
+ .then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
+ .then(() => UsersService.findById(mockUsers[0].id))
+ .then(() => {
+ throw new Error('Error expected');
+ })
+ .catch((err) => {
+ expect(err.status).to.equal(400);
+ expect(err.translation_key).to.equal('SAME_USERNAME_PROVIDED');
+ });
+ });
+
it('should return an error if canEditName is false', (done) => {
UsersService
.editName(mockUsers[0].id, 'Jojo')
diff --git a/views/admin.ejs b/views/admin.ejs
index 4ef4112da..54c666395 100644
--- a/views/admin.ejs
+++ b/views/admin.ejs
@@ -33,58 +33,6 @@
#root > div {
height: 100%;
}
-
- /* putting this here until I can get webpack to behave */
- .react-tagsinput {
- background-color: #fff;
- border: 1px solid #ccc;
- overflow: hidden;
- padding-left: 5px;
- padding-top: 5px;
- }
-
- .react-tagsinput--focused {
- border-color: rgb(142, 76, 65);
- }
-
- .react-tagsinput-tag {
- background-color: rgb(255, 220, 214);
- border-radius: 2px;
- border: 1px solid rgb(244, 126, 107);
- color: rgb(244, 126, 107);
- display: inline-block;
- font-family: sans-serif;
- font-size: 13px;
- font-weight: 400;
- margin-bottom: 5px;
- margin-right: 5px;
- padding: 5px;
- }
-
- .react-tagsinput-remove {
- cursor: pointer;
- font-weight: bold;
- color: rgb(101, 24, 23);
- }
-
- .react-tagsinput-tag a::before {
- content: " ×";
- }
-
- .react-tagsinput-input {
- background: transparent;
- border: 0;
- color: #777;
- font-family: sans-serif;
- font-size: 13px;
- font-weight: 400;
- margin-bottom: 6px;
- margin-top: 1px;
- outline: none;
- padding: 5px;
- width: 90px;
- }
-
<% if (data != null) { %>
diff --git a/yarn.lock b/yarn.lock
index df7efdaf8..92edf11b5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2041,7 +2041,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
create-hash "^1.1.0"
inherits "^2.0.1"
-create-react-class@^15.5.1:
+create-react-class@^15.5.1, create-react-class@^15.5.2:
version "15.5.2"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681"
dependencies:
@@ -6878,6 +6878,13 @@ react-highlight-words@^0.6.0:
dependencies:
highlight-words-core "^1.0.2"
+react-input-autosize@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.1.4.tgz#cbc45072d4084ddc57806db8e3b34e644b8366ac"
+ dependencies:
+ create-react-class "^15.5.2"
+ prop-types "^15.5.8"
+
react-linkify@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-0.1.3.tgz#6e886180bda6c8fdc5f9f8a7ebe82fc0f48db7ad"
@@ -6931,9 +6938,9 @@ react-router@^3.0.0:
prop-types "^15.5.6"
warning "^3.0.0"
-react-tagsinput@^3.14.0:
- version "3.16.1"
- resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.16.1.tgz#dfb3bcbe5fc4430f60c145716c17cdc2613ce117"
+react-tagsinput@^3.17.0:
+ version "3.17.0"
+ resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.17.0.tgz#d699bc7ff3be3492d5a3d2a57f6976908774fba3"
react-toastify@^1.5.0:
version "1.5.0"
@@ -7595,6 +7602,10 @@ smart-buffer@^1.0.4:
version "1.1.15"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
+smoothscroll-polyfill@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.3.5.tgz#466e6039b51cb525d70e1a5077ef81e064678eae"
+
smtp-connection@2.12.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/smtp-connection/-/smtp-connection-2.12.0.tgz#d76ef9127cb23c2259edb1e8349c2e8d5e2d74c1"