diff --git a/.eslintignore b/.eslintignore index e09212b62..4805e34a4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,5 +19,8 @@ plugins/* !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-author-menu +!plugins/talk-plugin-member-since +!plugins/talk-plugin-ignore-user node_modules diff --git a/.eslintrc.json b/.eslintrc.json index 12355ac34..8ca153cbc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,64 +3,61 @@ "es6": true, "node": true }, - "extends": "eslint:recommended", + "extends": [ + "eslint:recommended" + ], "parserOptions": { "ecmaVersion": 2017 }, + "plugins": [ + "promise", + "json" + ], "rules": { - "indent": ["error", - 2 - ], - "no-console": [ - 0 - ], + "indent": ["error", 2], + "no-console": "off", "linebreak-style": ["error", "unix"], "quotes": ["error", "single"], "semi": ["error", "always"], - "no-template-curly-in-string": [1], - "no-unsafe-negation": [1], - "array-callback-return": [1], + "no-template-curly-in-string": "warn", + "no-unsafe-negation": "warn", + "array-callback-return": "warn", "arrow-parens": ["warn", "always"], "template-curly-spacing": "warn", - "eqeqeq": [2, "smart"], - "no-eval": [2], - "no-global-assign": [2], - "no-implied-eval": [2], + "eqeqeq": ["error", "smart"], + "no-eval": "error", + "no-global-assign": "error", + "no-implied-eval": "error", "lines-around-comment": ["warn", {"beforeLineComment": true}], - "spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }], - "no-script-url": [2], - "no-throw-literal": [2], - "yoda": [1], - "no-path-concat": [2], - "eol-last": [1], - "no-nested-ternary": [1], - "no-tabs": [2], - "no-unneeded-ternary": [1], - "object-curly-spacing": [1], + "spaced-comment": ["warn", "always", {"line": {"exceptions": ["-", "="]}}], + "no-script-url": "error", + "no-throw-literal": "error", + "yoda": "warn", + "no-path-concat": "error", + "eol-last": "warn", + "no-nested-ternary": "warn", + "no-tabs": "error", + "no-unneeded-ternary": "warn", + "object-curly-spacing": "warn", "space-infix-ops": ["error"], "space-in-parens": ["error", "never"], - "space-unary-ops": ["error", { - "words": true, - "nonwords": false - }], - "no-const-assign": [2], - "no-duplicate-imports": [2], - "prefer-template": [1], - "comma-spacing": ["error", { - "after": true - }], - "no-var": [2], - "no-lonely-if": [2], - "curly": [2], - "no-unused-vars": ["error", { - "argsIgnorePattern": "^_|next", - "varsIgnorePattern": "^_" - }], - "no-multiple-empty-lines": ["error", { - "max": 1 - }], - "newline-per-chained-call": ["error", { - "ignoreChainWithDepth": 2 - }] + "space-unary-ops": ["error", {"words": true, "nonwords": false}], + "no-const-assign": "error", + "no-duplicate-imports": "error", + "prefer-template": "warn", + "comma-spacing": ["error", {"after": true}], + "no-var": "error", + "no-lonely-if": "error", + "curly": "error", + "no-unused-vars": ["error", {"argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_"}], + "no-multiple-empty-lines": ["error", {"max": 1}], + "newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}], + "promise/no-return-wrap": "error", + "promise/param-names": "error", + "promise/catch-or-return": "warn", + "promise/no-native": "off", + "promise/no-nesting": "warn", + "promise/no-promise-in-callback": "warn", + "promise/no-callback-in-promise": "warn" } } diff --git a/.gitignore b/.gitignore index 3fdb05c89..0a2ac00df 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,8 @@ plugins/* !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-author-menu +!plugins/talk-plugin-member-since +!plugins/talk-plugin-ignore-user **/node_modules/* diff --git a/README.md b/README.md index a58ac7c74..4bec86d07 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html) @@ -19,7 +20,7 @@ endpoint when the server is running with built assets. - Blog: https://blog.coralproject.net -- Community Guides for Journalism: https://guides.coralproject.net/ +- Community Guides for Journalism: https://guides.coralproject.net/ ## License diff --git a/app.json b/app.json index 5faa23bc2..6c85e3cc2 100644 --- a/app.json +++ b/app.json @@ -1,10 +1,15 @@ { "name": "The Coral Project: Talk", "env": { - "TALK_SESSION_SECRET": { + "TALK_JWT_SECRET": { "description": "The session secret", "generator": "secret" }, + "TALK_ROOT_URL": { + "description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.", + "value":"https://.herokuapp.com", + "required": true + }, "TALK_FACEBOOK_APP_ID": { "value": "", "required": true @@ -14,8 +19,7 @@ "required": true }, "NODE_ENV": "production", - "TALK_SMTP_PORT": "2525", - "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN", + "REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" }, "addons": [{ @@ -25,8 +29,8 @@ "plan": "rediscloud:30", "as": "REDIS" }, { - "plan": "postmark:10k", - "as": "POSTMARK" + "plan": "mailgun:starter", + "as": "MAILGUN" }], "image": "heroku/nodejs", "success_url": "/admin/install" diff --git a/bin/cli b/bin/cli index 4eee09f70..f618b24be 100755 --- a/bin/cli +++ b/bin/cli @@ -4,8 +4,9 @@ * Module dependencies. */ -// const util = require('./util'); const program = require('./commander'); +const {head, map} = require('lodash'); +const Matcher = require('did-you-mean'); program .command('serve', 'serve the application') @@ -23,6 +24,21 @@ program ) .parse(process.argv); +// If the command wasn't found, output help. +const cmds = map(program.commands, '_name'); +const cmd = head(program.args); +if (!cmds.includes(cmd)) { + const m = new Matcher(cmds); + const similarCMDs = m.list(cmd); + + console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`); + if (similarCMDs.length > 0) { + const sc = similarCMDs.map(({value}) => `\t${value}\n`).join(''); + console.error(`\nThe most similar commands are\n${sc}`); + } + process.exit(1); +} + /** * When this provess exists, check to see if we have a running command, if we do * check to see if it is still running. If it is, then kill it with a SIGINT @@ -31,6 +47,8 @@ program */ process.once('exit', () => { if ( + + // program.runningCommand && program.runningCommand.killed === false && program.runningCommand.exitCode === null ) { diff --git a/bin/cli-jobs b/bin/cli-jobs index f22e1068b..c5aefc518 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -78,18 +78,23 @@ function rangeJobsByState(state = 'complete', limit) { /** * Cleans up the jobs that are in the queue. */ -function cleanupJobs(options) { +async function cleanupJobs(options) { const n = 100; - Promise.all([ - rangeJobsByState('complete', n), - options.stuck ? rangeJobsByState('failed', n) : false - ]) - .then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs)) - .then(() => { + try { + const joblists = await Promise.all([ + rangeJobsByState('complete', n), + options.stuck ? rangeJobsByState('failed', n) : false + ]); + + await joblists.filter((jobs) => jobs).map(removeJobs); + util.shutdown(); console.log('Removed old jobs'); - }); + } catch (err) { + console.error(err); + util.shutdown(1); + } } //============================================================================== diff --git a/bin/cli-plugins b/bin/cli-plugins index 1de1c97d4..b7e301fce 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -53,7 +53,7 @@ function versionMatch(name, version) { } } -const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc. +const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc. function reconcilePackages({quiet = false, upgradeRemote = false}) { const fetchable = []; diff --git a/bin/cli-users b/bin/cli-users index 057c0a806..c6555ab28 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -98,39 +98,32 @@ function getUserCreateAnswers(options) { /** * Prompts for input and registers a user based on those. */ -function createUser(options) { - getUserCreateAnswers(options) - .then((answers) => { - if (answers.password !== answers.confirmPassword) { - return Promise.reject(new Error('Passwords do not match')); - } +async function createUser(options) { + try { + const answers = await getUserCreateAnswers(options); + if (answers.password !== answers.confirmPassword) { + throw new Error('Passwords do not match'); + } - return answers; - }) - .then((answers) => { - return UsersService - .createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim()) - .then((user) => { - console.log(`Created user ${user.id}.`); + const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim()); + console.log(`Created user ${user.id}.`); - if (answers.roles.length > 0) { - return Promise.all(answers.roles.map((role) => { - return UsersService - .addRoleToUser(user.id, role) - .then(() => { - console.log(`Added the role ${role} to User ${user.id}.`); - }); - })); - } - }); - }) - .then(() => { - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(); - }); + if (answers.roles.length > 0) { + return Promise.all(answers.roles.map((role) => { + return UsersService + .addRoleToUser(user.id, role) + .then(() => { + console.log(`Added the role ${role} to User ${user.id}.`); + }); + })); + } + + util.shutdown(); + + } catch (err) { + console.error(err); + util.shutdown(); + } } /** @@ -169,21 +162,21 @@ function passwd(userID) { validate: validateRequired('Confirm Password is required') } ]) - .then((answers) => { - if (answers.password !== answers.confirmPassword) { - return Promise.reject(new Error('Password mismatch')); - } + .then((answers) => { + if (answers.password !== answers.confirmPassword) { + throw new Error('Password mismatch'); + } - return UsersService.changePassword(userID, answers.password); - }) - .then(() => { - console.log('Password changed.'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + return UsersService.changePassword(userID, answers.password); + }) + .then(() => { + console.log('Password changed.'); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); } /** diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comments.js index f7dd4d397..231badabb 100644 --- a/bin/verifications/database/comments.js +++ b/bin/verifications/database/comments.js @@ -125,7 +125,7 @@ module.exports = async ({fix, limit, batch}) => { // Check that the action summaries match the cached counts. if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) { - // Batch updates for those changes. + // Batch updates for those changes. commentOperations.push({ [`action_counts.${ACTION_COUNT_FIELD}`]: count, }); diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 5735a91a1..cde3942fa 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -4,6 +4,7 @@ "es6": true, "mocha": true }, + "extends": "../.eslintrc.json", "parserOptions": { "sourceType": "module", "ecmaFeatures": { diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 402a32e5d..890d834b4 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -52,16 +52,16 @@ export const newPage = () => ({ export const setRole = (id, role) => (dispatch, _, {rest}) => { return rest(`/users/${id}/role`, {method: 'POST', body: {role}}) - .then(() => { - return dispatch({type: SET_ROLE, id, role}); - }); + .then(() => { + return dispatch({type: SET_ROLE, id, role}); + }); }; export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => { return rest(`/users/${id}/status`, {method: 'POST', body: {status}}) - .then(() => { - return dispatch({type: SET_COMMENTER_STATUS, id, status}); - }); + .then(() => { + return dispatch({type: SET_COMMENTER_STATUS, id, status}); + }); }; // Ban User Dialog diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index d7210782a..02d562d65 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -26,19 +26,19 @@ const validation = (formData, dispatch, next) => { // Required Validation const empty = validKeys - .filter((name) => { - const cond = !formData[name].length; + .filter((name) => { + const cond = !formData[name].length; - if (cond) { + if (cond) { - // Adding Error - dispatch(addError(name, 'This field is required.')); - } else { - dispatch(addError(name, '')); - } + // Adding Error + dispatch(addError(name, 'This field is required.')); + } else { + dispatch(addError(name, '')); + } - return cond; - }); + return cond; + }); if (empty.length) { dispatch(hasError()); @@ -128,18 +128,18 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST}); const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed}); const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error}); -export const checkInstall = (next) => (dispatch, _, {rest}) => { +export const checkInstall = (next) => async (dispatch, _, {rest}) => { dispatch(checkInstallRequest()); - rest('/setup') - .then(({installed}) => { - dispatch(checkInstallSuccess(installed)); - if (installed) { - next(); - } - }) - .catch((error) => { - console.error(error); - const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch(checkInstallFailure(errorMessage)); - }); + + try { + const {installed} = await rest('/setup'); + dispatch(checkInstallSuccess(installed)); + if (installed) { + next(); + } + } catch (error) { + console.error(error); + const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); + dispatch(checkInstallFailure(errorMessage)); + } }; diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js index ce832be8f..5f9712127 100644 --- a/client/coral-admin/src/components/AdminLogin.js +++ b/client/coral-admin/src/components/AdminLogin.js @@ -70,23 +70,23 @@ class AdminLogin extends React.Component { ); const requestPasswordForm = ( this.props.passwordRequestSuccess - ?

{ - location.href = location.href; - }}> + ?

{ + location.href = location.href; + }}> {this.props.passwordRequestSuccess} Sign in

- :
- this.setState({email: e.target.value})} /> - - + :
+ this.setState({email: e.target.value})} /> + + ); return ( diff --git a/client/coral-admin/src/components/SuspendUserDialog.js b/client/coral-admin/src/components/SuspendUserDialog.js index bb0cff84b..d56201e2f 100644 --- a/client/coral-admin/src/components/SuspendUserDialog.js +++ b/client/coral-admin/src/components/SuspendUserDialog.js @@ -66,7 +66,7 @@ class SuspendUserDialog extends React.Component { {t('suspenduser.title_suspend')}

- {t('suspenduser.description_suspend', username)} + {t('suspenduser.description_suspend', username)}

{t('suspenduser.select_duration')} @@ -104,7 +104,7 @@ class SuspendUserDialog extends React.Component { {t('suspenduser.title_notify')}

- {t('suspenduser.description_notify', username)} + {t('suspenduser.description_notify', username)}

{t('suspenduser.write_message')} diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 11cd444e1..fd44d5f17 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -28,16 +28,26 @@ export default class UserDetail extends React.Component { bulkReject: PropTypes.func.isRequired, } - rejectThenReload = (info) => { - this.props.rejectComment(info).then(() => { + rejectThenReload = async (info) => { + try { + await this.props.rejectComment(info); this.props.data.refetch(); - }); + } catch (err) { + + // TODO: handle error. + console.error(err); + } } - acceptThenReload = (info) => { - this.props.acceptComment(info).then(() => { + acceptThenReload = async (info) => { + try { + await this.props.acceptComment(info); this.props.data.refetch(); - }); + } catch (err) { + + // TODO: handle error. + console.error(err); + } } showAll = () => { @@ -133,35 +143,35 @@ export default class UserDetail extends React.Component {
{ selectedCommentIds.length === 0 - ? ( -
    -
  • All
  • -
  • Rejected
  • -
- ) - : ( -
- - - {`${selectedCommentIds.length} comments selected`} -
- ) + ? ( +
    +
  • All
  • +
  • Rejected
  • +
+ ) + : ( +
+ + + {`${selectedCommentIds.length} comments selected`} +
+ ) }
@@ -190,7 +200,7 @@ export default class UserDetail extends React.Component { className={styles.loadMore} loadMore={loadMore} showLoadMore={hasNextPage} - /> + /> ); diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index c8daa8019..fa196dc41 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -55,8 +55,8 @@ class UserDetailComment extends React.Component { { (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null + ?  ({t('comment.edited')}) + : null }
@@ -122,10 +122,10 @@ class UserDetailComment extends React.Component {
{flagActions && flagActions.length ? + actions={flagActions} + actionSummaries={flagActionSummaries} + viewUserDetail={viewUserDetail} + /> : null} ); diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 0b4ff0e5d..560935c2a 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -14,9 +14,9 @@ const CoralHeader = ({ }) => (
-
- { - auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? +
+ { + auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? : null - } -
-
+
); diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index 2c5e1621a..f2d71c93b 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -55,10 +55,10 @@ class LayoutContainer extends Component { toggleShortcutModal={toggleShortcutModal} {...this.props} > - - - - {this.props.children} + + + + {this.props.children} ); } else if (loggedIn) { diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js index f60014832..1616acfcf 100644 --- a/client/coral-admin/src/containers/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/SuspendUserDialog.js @@ -54,8 +54,9 @@ class SuspendUserDialogContainer extends Component { const withOrganizationName = withQuery(gql` query CoralAdmin_SuspendUserDialog { + __typename settings { - organizationName + organizationName } } `); diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index e07b47261..5effb2b84 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -36,14 +36,19 @@ class UserDetailContainer extends React.Component { isLoadingMore = false; // status can be 'ACCEPTED' or 'REJECTED' - bulkSetCommentStatus = (status) => { + bulkSetCommentStatus = async (status) => { const changes = this.props.selectedCommentIds.map((commentId) => { return this.props.setCommentStatus({commentId, status}); }); - Promise.all(changes).then(() => { + try { + await Promise.all(changes); this.props.clearUserDetailSelections(); // un-select everything - }); + } catch (err) { + + // TODO: handle error. + console.error(err); + } } bulkReject = () => { @@ -88,13 +93,13 @@ class UserDetailContainer extends React.Component { }); } }) - .then(() => { - this.isLoadingMore = false; - }) - .catch((err) => { - this.isLoadingMore = false; - throw err; - }); + .then(() => { + this.isLoadingMore = false; + }) + .catch((err) => { + this.isLoadingMore = false; + throw err; + }); }; componentWillReceiveProps(next) { diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index dea68c5e8..8fedf6ebe 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -1,10 +1,33 @@ +import update from 'immutability-helper'; + export default { mutations: { - SetUserStatus: () => ({ - refetchQueries: ['CoralAdmin_Community'], + SetUserStatus: ({variables: {status, userId}}) => ({ + updateQueries: { + TalkAdmin_FlaggedAccounts: (prev) => { + if (status !== 'APPROVED') { + return prev; + } + const updated = update(prev, { + users: { + nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)}, + }, + }); + return updated; + } + } }), - RejectUsername: () => ({ - refetchQueries: ['CoralAdmin_Community'], + RejectUsername: ({variables: {input: {id: userId}}}) => ({ + updateQueries: { + TalkAdmin_FlaggedAccounts: (prev) => { + const updated = update(prev, { + users: { + nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)}, + }, + }); + return updated; + } + } }), }, }; diff --git a/client/coral-admin/src/routes/Community/components/ActionButton.css b/client/coral-admin/src/routes/Community/components/ActionButton.css new file mode 100644 index 000000000..8f328e8b8 --- /dev/null +++ b/client/coral-admin/src/routes/Community/components/ActionButton.css @@ -0,0 +1,4 @@ +.actionButton { + transform: scale(.8); + margin: 0; +} diff --git a/client/coral-admin/src/routes/Community/components/ActionButton.js b/client/coral-admin/src/routes/Community/components/ActionButton.js index 09af420f6..0ef9b8a39 100644 --- a/client/coral-admin/src/routes/Community/components/ActionButton.js +++ b/client/coral-admin/src/routes/Community/components/ActionButton.js @@ -1,10 +1,11 @@ import React from 'react'; -import styles from './Community.css'; +import styles from './ActionButton.css'; import {Button} from 'coral-ui'; import {menuActionsMap} from '../../../utils/moderationQueueActionsMap'; import t from 'coral-framework/services/i18n'; +// TODO: Needs refactoring. const ActionButton = ({type = '', user, ...props}) => { return ( + {me.id !== user.id && + + + Suspend User + + + Ban User + + + } +
+ + +
+
+
+ flag + + {t('community.flags')}({ user.actions.length }) + : + { user.action_summaries.map( + (action, i) => { + return + {shortReasons[action.reason]} ({action.count}) + ; + } + )} +
+
+ { user.action_summaries.map( + (action_sum, i) => { + return
+ + {shortReasons[action_sum.reason]} ({action_sum.count}) + + {user.actions.map( + + // find the action by action_sum.reason + (action, j) => { + if (action.reason === action_sum.reason) { + return

+ {action.user && + + } + : {action.message ? action.message : 'n/a'} +

; + } + return null; + } + )} +
; + } + )} + +
+
+
+
+ {modActionButtons.map((action, i) => + + )} +
+
+
+ + + ); + } +} + +export default User; diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 464f3bc5b..ff1c2c279 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -1,6 +1,6 @@ import React from 'react'; -import styles from './Community.css'; +import styles from './styles.css'; import Table from '../containers/Table'; import {Pager, Icon} from 'coral-ui'; import EmptyCard from '../../../components/EmptyCard'; @@ -45,12 +45,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
{ hasResults - ? - : {t('community.no_results')} + : {t('community.no_results')} } this.setState({stage: stage + 1}); - const suspend = () => { - rejectUsername({id: user.user.id, message: this.state.email}) - .then(() => { + const suspend = async () => { + try { + await rejectUsername({id: user.user.id, message: this.state.email}); this.props.handleClose(); - }); + } catch (err) { + + // TODO: handle error. + console.error(err); + } }; const suspendModalActions = [ @@ -72,21 +76,21 @@ class RejectUsernameDialog extends Component { const {stage} = this.state; return -
- {t(stages[stage].title, t('reject_username.username'))} -
-
-
- {t(stages[stage].description, t('reject_username.username'))} -
- { - stage === 1 && + className={styles.suspendDialog} + id="rejectUsernameDialog" + open={open} + onClose={handleClose} + onCancel={handleClose} + title={t('reject_username.suspend_user')}> +
+ {t(stages[stage].title, t('reject_username.username'))} +
+
+
+ {t(stages[stage].description, t('reject_username.username'))} +
+ { + stage === 1 &&
{t('reject_username.write_message')}
@@ -97,16 +101,16 @@ class RejectUsernameDialog extends Component { onChange={this.onEmailChange}/>
- } -
- {Object.keys(stages[stage].options).map((key, i) => ( - - ))} -
-
-
; + } +
+ {Object.keys(stages[stage].options).map((key, i) => ( + + ))} +
+ + ; } } diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index f851b1f84..c5bb494c0 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -9,9 +9,9 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
{headers.map((header, i) =>( ))} diff --git a/client/coral-admin/src/routes/Community/components/User.js b/client/coral-admin/src/routes/Community/components/User.js deleted file mode 100644 index 2d921a1c7..000000000 --- a/client/coral-admin/src/routes/Community/components/User.js +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import styles from './Community.css'; - -import ActionButton from './ActionButton'; -import {username} from 'talk-plugin-flags/helpers/flagReasons'; -import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; -import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; - -import t from 'coral-framework/services/i18n'; - -const shortReasons = { - [username.other]: t('community.other'), - [username.spam]: t('community.spam_ads'), - [username.offensive]: t('community.offensive'), - [username.nolike]: t('community.dont_like_username'), - [username.impersonating]: t('community.impersonating'), -}; - -// Render a single user for the list -const User = (props) => { - const {user, modActionButtons} = props; - let userStatus = user.status; - - const showSuspenUserDialog = () => props.showSuspendUserDialog({ - userId: user.id, - username: user.username, - }); - - const showBanUserDialog = () => props.showBanUserDialog({ - userId: user.id, - username: user.username, - }); - - // Do not display unless the user status is 'pending' or 'banned'. - // This means that they have already been reviewed and approved. - return (userStatus === 'PENDING' || userStatus === 'BANNED') && -
  • -
    -
    -
    - - {props.currentUser.id !== user.id && - - - Suspend User - - Ban User - - - } -
    -
    - -
    -
    -
    - flag{t('community.flags')}({ user.actions.length }): - { user.action_summaries.map( - (action, i) => { - return - {shortReasons[action.reason]} ({action.count}) - ; - } - )} -
    -
    - { user.action_summaries.map( - (action_sum, i) => { - return
    - - {shortReasons[action_sum.reason]} ({action_sum.count}) - - {user.actions.map( - - // find the action by action_sum.reason - (action, j) => { - if (action.reason === action_sum.reason) { - return

    - {action.user && - - } - : {action.message ? action.message : 'n/a'} -

    ; - } - return null; - } - )} -
    ; - } - )} - -
    -
    -
    -
    - {modActionButtons.map((action, i) => - - )} -
    -
    -
    -
    -
  • ; -}; - -export default User; diff --git a/client/coral-admin/src/routes/Community/components/styles.css b/client/coral-admin/src/routes/Community/components/styles.css index 8581c4c7d..fc94717e0 100644 --- a/client/coral-admin/src/routes/Community/components/styles.css +++ b/client/coral-admin/src/routes/Community/components/styles.css @@ -1,119 +1,56 @@ +/** + * deprecated as this file contains styles from multiple components. Please remove this file + * when styles have been refactored. + */ + @custom-media --big-viewport (min-width: 780px); -.listContainer { - max-width: 860px; - margin: 0 auto; -} - -.tabBar { - background-color: rgba(44, 44, 44, 0.89); - z-index: 5; -} - -.tab { - flex: 1; - color: white; - text-transform: capitalize; - font-weight: 500; - font-size: 15px; - letter-spacing: 1px; - transition: border-bottom 200ms; -} - -.active { - color: white; - box-sizing: border-box; - border-bottom: solid 5px #F36451; -} - -.active > span { - color: white; -} - -.active:after { - background: transparent !important; -} - -.showShortcuts { - position: absolute; - right: 130px; +.container { + padding: 10px; display: flex; - align-items: center; - font-size: 13px; - -span { - margin-left: 7px; -} + padding-bottom: 200px; } -@media (--big-viewport) { - .tab { - flex: none; - } +.leftColumn { + padding: 42px 56px; + width: 234px; } -.approve { - margin-top: 10px; +.mainContent { + width: calc(100% - 300px); + padding: 34px 14px; + box-sizing: border-box; } -.notFound { - position: relative; - margin: 20px auto; - text-align: center; - padding: 68px 45px; - vertical-align: middle; - min-width: 500px; - - a { - color: rgb(244, 126, 107); - font-weight: 500; - - &.goToStreams { - position: absolute; - right: 10px; - bottom: 10px; - } - } +.roleButton { + display: block; } -.header { - background-color: #2c2c2c; - color: white; - margin-bottom: -1px; +.searchBox { + width: 100%; + padding: 9px; + border: 1px solid #ccc; + border-radius: 2px; + display: flex; + background: white; + box-sizing: border-box; + height: 40px; - .settingsButton { - i { - vertical-align: middle; - margin-left: 10px; - margin-top: -4px; - } + i { + color: #A1A1A1 } - .moderateAsset { - a { - text-align: center; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; - color: white; - text-transform: capitalize; - font-weight: 500; - font-size: 15px; - letter-spacing: 1px; - transition: opacity 200ms; - opacity: 1; - - &:hover { - opacity: .8; - cursor: pointer; - } - } + input { + display: block; + width: 100%; + height: 100%; + border: none; + font-size: 16px; + padding: 0 2px 0 15px; + box-sizing: border-box; } } - -@custom-media --big-viewport (min-width: 780px); - .list { padding: 8px 0; list-style: none; @@ -134,15 +71,12 @@ span { position: fixed; bottom: 60px; left: 25%; + margin: 0 auto; display: flex; justify-content: space-around; width: 50%; margin: 0; } - - .actionButton { - transform: scale(1.4); - } } } @@ -159,6 +93,7 @@ span { min-height: 220px; .container { + display: block; padding: 0 14px; min-height: 180px; } @@ -184,9 +119,9 @@ span { right: 0; height: 100%; top: 0; - padding: 40px 18px; + padding: 50px 18px; box-sizing: border-box; - } + } .itemHeader { display: flex; @@ -194,6 +129,8 @@ span { justify-content: space-between; .author { + font-size: 16px; + font-weight: bold; min-width: 230px; display: flex; align-items: center; @@ -221,18 +158,12 @@ span { margin-left: 40px; } - .actionButton { - transform: scale(.8); - margin: 0; - } - .body { margin-top: 0px; flex: 1; color: black; max-width: 500px; word-wrap: break-word; - font-weight: 300; } .flagged { @@ -245,11 +176,8 @@ span { font-size: 12px; color: #d32f2f; } - } - - .empty { color: #444; margin-top: 50px; @@ -301,34 +229,14 @@ span { margin-top: 5px; } -.Comment { - .moderateArticle { - font-size: 12px; - a { - display: inline-block; - color: #679af3; - text-decoration: none; - font-size: 1em; - font-weight: 400; - letter-spacing: .5px; - font-size: 12px; - margin-left: 10px; +.banButton { + width: 114px; + letter-spacing: 1px; - &:hover { - text-decoration: underline; - opacity: .9; - cursor: pointer; - } - } - } -} - -.flagBox { - max-width: 480px; - border-top: 1px solid rgba(66, 66, 66, 0.12); - h3 { + i { + vertical-align: middle; + margin-right: 10px; font-size: 14px; - margin: 0; - font-weight: 500; } } + diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index b04fc0281..a41c257d6 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -1,23 +1,15 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {compose, gql} from 'react-apollo'; -import withQuery from 'coral-framework/hocs/withQuery'; -import {Spinner} from 'coral-ui'; +import {compose} from 'react-apollo'; import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations'; - -import {showBanUserDialog} from 'actions/banUserDialog'; -import {showSuspendUserDialog} from 'actions/suspendUserDialog'; - import { fetchAccounts, updateSorting, newPage, - showRejectUsernameDialog, hideRejectUsernameDialog } from '../../../actions/community'; -import {viewUserDetail} from '../../../actions/userDetail'; import Community from '../components/Community'; @@ -27,85 +19,26 @@ class CommunityContainer extends Component { this.props.fetchAccounts({}); } - approveUser = ({userId}) => { - return this.props.setUserStatus({userId, status: 'APPROVED'}); - } - - banUser = ({userId}) => { - return this.props.setUserStatus({userId, status: 'BANNED'}); - } - render() { - if (this.props.data.error) { - return
    {this.props.data.error.message}
    ; - } - - if (!('users' in this.props.root)) { - return
    ; - } return ( - + ); } } - -export const withCommunityQuery = withQuery(gql` - query CoralAdmin_Community($action_type: ACTION_TYPE) { - users(query:{action_type: $action_type}){ - id - username - status - roles - actions{ - id - created_at - ... on FlagAction { - reason - message - user { - id - username - } - } - } - action_summaries { - count - ... on FlagActionSummary { - reason - } - } - } - } -`, { - options: ({params: {action_type = 'FLAG'}}) => { - return { - variables: { - action_type: action_type - } - }; - } -}); - const mapStateToProps = (state) => ({ community: state.community, - currentUser: state.auth.user, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchAccounts, - showBanUserDialog, - showSuspendUserDialog, - showRejectUsernameDialog, hideRejectUsernameDialog, updateSorting, newPage, - viewUserDetail, }, dispatch); export default compose( connect(mapStateToProps, mapDispatchToProps), - withCommunityQuery, withSetUserStatus, withRejectUsername, )(CommunityContainer); diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js new file mode 100644 index 000000000..00c6fdf33 --- /dev/null +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -0,0 +1,128 @@ +import React, {Component} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {compose, gql} from 'react-apollo'; +import withQuery from 'coral-framework/hocs/withQuery'; +import {Spinner} from 'coral-ui'; + +import {withSetUserStatus} from 'coral-framework/graphql/mutations'; +import {showBanUserDialog} from 'actions/banUserDialog'; +import {showSuspendUserDialog} from 'actions/suspendUserDialog'; +import {showRejectUsernameDialog} from '../../../actions/community'; +import {viewUserDetail} from '../../../actions/userDetail'; +import {getDefinitionName} from 'coral-framework/utils'; +import {appendNewNodes} from 'plugin-api/beta/client/utils'; +import update from 'immutability-helper'; + +import FlaggedAccounts from '../components/FlaggedAccounts'; +import FlaggedUser from '../containers/FlaggedUser'; + +class FlaggedAccountsContainer extends Component { + + constructor(props) { + super(props); + } + + approveUser = ({userId}) => { + return this.props.setUserStatus({userId, status: 'APPROVED'}); + } + + loadMore = () => { + return this.props.data.fetchMore({ + query: LOAD_MORE_QUERY, + variables: { + limit: 5, + cursor: this.props.root.users.endCursor, + }, + updateQuery: (previous, {fetchMoreResult:{users}}) => { + const updated = update(previous, { + users: { + nodes: { + $apply: (nodes) => appendNewNodes(nodes, users.nodes), + }, + hasNextPage: {$set: users.hasNextPage}, + endCursor: {$set: users.endCursor}, + }, + }); + return updated; + }, + }); + }; + + render() { + if (this.props.data.error) { + return
    {this.props.data.error.message}
    ; + } + + if (this.props.data.loading) { + return
    ; + } + return ( + + ); + } +} + +const LOAD_MORE_QUERY = gql` + query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) { + users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){ + hasNextPage + endCursor + nodes { + __typename + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + } + ${FlaggedUser.fragments.user} +`; + +export const withFlaggedAccountsyQuery = withQuery(gql` + query TalkAdmin_FlaggedAccounts { + ...${getDefinitionName(FlaggedUser.fragments.root)} + users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){ + hasNextPage + endCursor + nodes { + __typename + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + me { + __typename + ...${getDefinitionName(FlaggedUser.fragments.me)} + } + } + ${FlaggedUser.fragments.root} + ${FlaggedUser.fragments.user} + ${FlaggedUser.fragments.me} +`, { + options: { + fetchPolicy: 'network-only', + }, +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + showBanUserDialog, + showSuspendUserDialog, + showRejectUsernameDialog, + viewUserDetail, + }, dispatch); + +export default compose( + connect(null, mapDispatchToProps), + withFlaggedAccountsyQuery, + withSetUserStatus, +)(FlaggedAccountsContainer); diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedUser.js b/client/coral-admin/src/routes/Community/containers/FlaggedUser.js new file mode 100644 index 000000000..f92c93c1c --- /dev/null +++ b/client/coral-admin/src/routes/Community/containers/FlaggedUser.js @@ -0,0 +1,42 @@ +import {gql} from 'react-apollo'; +import FlaggedUser from '../components/FlaggedUser'; +import {withFragments} from 'plugin-api/beta/client/hocs'; + +export default withFragments({ + root: gql` + fragment TalkAdminCommunity_FlaggedUser_root on RootQuery { + __typename + } + `, + me: gql` + fragment TalkAdminCommunity_FlaggedUser_me on User { + id + } + `, + user: gql` + fragment TalkAdminCommunity_FlaggedUser_user on User { + id + username + status + roles + actions{ + id + created_at + ... on FlagAction { + reason + message + user { + id + username + } + } + } + action_summaries { + count + ... on FlagActionSummary { + reason + } + } + } + ` +})(FlaggedUser); diff --git a/client/coral-admin/src/routes/Configure/components/Configure.css b/client/coral-admin/src/routes/Configure/components/Configure.css index 5b02b963a..0b7319132 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.css +++ b/client/coral-admin/src/routes/Configure/components/Configure.css @@ -1,3 +1,7 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please refactor. + */ + .container { display: flex; diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index 84e47dfb7..15ff69b69 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -111,20 +111,20 @@ export default class Configure extends Component { (bool, error) => this.state.errors[error] ? false : bool, this.state.changed); return ( -
    -
    - - - {t('configure.stream_settings')} - - - {t('configure.moderation_settings')} - - - {t('configure.tech_settings')} - - -
    +
    +
    + + + {t('configure.stream_settings')} + + + {t('configure.moderation_settings')} + + + {t('configure.tech_settings')} + + +
    { showSave ? - : + : + {t('configure.save_changes')} + } -
    +
    -
    -
    - { this.props.saveFetchingError } - { this.props.fetchSettingsError } - { section } -
    +
    + { this.props.saveFetchingError } + { this.props.fetchSettingsError } + { section } +
    +
    ); } } diff --git a/client/coral-admin/src/routes/Configure/components/EmbedLink.js b/client/coral-admin/src/routes/Configure/components/EmbedLink.js index 4506d2f06..be0a13d5e 100644 --- a/client/coral-admin/src/routes/Configure/components/EmbedLink.js +++ b/client/coral-admin/src/routes/Configure/components/EmbedLink.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import t from 'coral-framework/services/i18n'; +import {join} from 'path'; import styles from './Configure.css'; import {Button, Card} from 'coral-ui'; import {BASE_URL} from 'coral-framework/constants/url'; @@ -25,7 +26,7 @@ class EmbedLink extends Component { } render () { - const coralJsUrl = [BASE_URL, '/embed.js'].join(''); + const coralJsUrl = join(BASE_URL, '/embed.js'); const nonce = String(Math.random()).slice(2); const streamElementId = `coral_talk_${nonce}`; const embedText = ` diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js index 9c216a853..1427f949e 100644 --- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js @@ -93,15 +93,15 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => { value={settings.charCount} disabled={settings.charCountEnable ? '' : 'disabled'} /> - {t('configure.comment_count_text_post')} - { - errors.charCount && + {t('configure.comment_count_text_post')} + { + errors.charCount &&
    {t('configure.comment_count_error')}
    - } + }

    diff --git a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js index 393b670a7..3f8af141b 100644 --- a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js @@ -15,19 +15,19 @@ const ActivityWidget = ({assets}) => {
    { assets.length - ? assets.map((asset) => { - return ( -
    - Moderate -

    {asset.commentCount}

    - -

    {asset.title}

    -
    -

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    -
    - ); - }) - :
    {t('dashboard.no_activity')}
    + ? assets.map((asset) => { + return ( +
    + Moderate +

    {asset.commentCount}

    + +

    {asset.title}

    +
    +

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    +
    + ); + }) + :
    {t('dashboard.no_activity')}
    }
    diff --git a/client/coral-admin/src/routes/Dashboard/components/Dashboard.css b/client/coral-admin/src/routes/Dashboard/components/Dashboard.css index cdad990ba..586ff6c08 100644 --- a/client/coral-admin/src/routes/Dashboard/components/Dashboard.css +++ b/client/coral-admin/src/routes/Dashboard/components/Dashboard.css @@ -1,3 +1,7 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please refactor. + */ + .Dashboard { display: flex; } diff --git a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js index 1cf011cef..2f3548cf1 100644 --- a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js @@ -17,24 +17,24 @@ const FlagWidget = ({assets}) => {
    { assets.length - ? assets.map((asset) => { - let flagSummary = null; - if (asset.action_summaries) { - flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary'); - } + ? assets.map((asset) => { + let flagSummary = null; + if (asset.action_summaries) { + flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary'); + } - return ( -
    - Moderate -

    {flagSummary ? flagSummary.actionCount : 0}

    - -

    {asset.title}

    -
    -

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    -
    - ); - }) - :
    {t('dashboard.no_flags')}
    + return ( +
    + Moderate +

    {flagSummary ? flagSummary.actionCount : 0}

    + +

    {asset.title}

    +
    +

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    +
    + ); + }) + :
    {t('dashboard.no_flags')}
    }
    diff --git a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js index 3c61cd1a3..7525f7223 100644 --- a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js @@ -16,20 +16,20 @@ const LikeWidget = ({assets}) => {
    { assets.length - ? assets.map((asset) => { - const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary'); - return ( -
    - Moderate -

    {likeSummary ? likeSummary.actionCount : 0}

    - -

    {asset.title}

    -
    -

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    -
    - ); - }) - :
    {t('dashboard.no_likes')}
    + ? assets.map((asset) => { + const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary'); + return ( +
    + Moderate +

    {likeSummary ? likeSummary.actionCount : 0}

    + +

    {asset.title}

    +
    +

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    +
    + ); + }) + :
    {t('dashboard.no_likes')}
    }
    diff --git a/client/coral-admin/src/routes/Dashboard/components/Widget.css b/client/coral-admin/src/routes/Dashboard/components/Widget.css index cf83fcbdc..e94d2313f 100644 --- a/client/coral-admin/src/routes/Dashboard/components/Widget.css +++ b/client/coral-admin/src/routes/Dashboard/components/Widget.css @@ -1,3 +1,7 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please refactor. + */ + :root { --row-height: 60px; } diff --git a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js index fe3680c76..2bb192f2e 100644 --- a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js +++ b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js @@ -42,15 +42,15 @@ export const witDashboardQuery = withQuery(gql` } } `, { - options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => { - return { - variables: { - from: dashboardWindowStart, - to: dashboardWindowEnd - } - }; - } -}); + options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => { + return { + variables: { + from: dashboardWindowStart, + to: dashboardWindowEnd + } + }; + } + }); const mapStateToProps = (state) => { return { diff --git a/client/coral-admin/src/routes/Install/components/style.css b/client/coral-admin/src/routes/Install/components/Install.css similarity index 50% rename from client/coral-admin/src/routes/Install/components/style.css rename to client/coral-admin/src/routes/Install/components/Install.css index 62f7ffe04..8e6f7504d 100644 --- a/client/coral-admin/src/routes/Install/components/style.css +++ b/client/coral-admin/src/routes/Install/components/Install.css @@ -1,12 +1,12 @@ -.Install { +.install { max-width: 900px; margin: 0 auto; text-align: center; padding: 50px 0; +} - h2 { - font-size: 2em; - font-weight: 500; - margin: 0; - } +.header { + font-size: 2em; + font-weight: 500; + margin: 0; } diff --git a/client/coral-admin/src/routes/Install/components/Install.js b/client/coral-admin/src/routes/Install/components/Install.js index f7069d67c..0d206d8f0 100644 --- a/client/coral-admin/src/routes/Install/components/Install.js +++ b/client/coral-admin/src/routes/Install/components/Install.js @@ -1,5 +1,5 @@ import React, {Component} from 'react'; -import styles from './style.css'; +import styles from './Install.css'; import {Wizard, WizardNav} from 'coral-ui'; import Layout from 'coral-admin/src/components/ui/Layout'; @@ -37,11 +37,11 @@ export default class Install extends Component { return ( -
    +
    { !install.alreadyInstalled ? (
    -

    Welcome to the Coral Project

    +

    Welcome to the Coral Project

    { install.step !== 0 ? : null } { showErrors={install.showErrors} errorMsg={install.errors.email} noValidate - /> + /> { onChange={handleUserChange} showErrors={install.showErrors} errorMsg={install.errors.username} - /> + /> { onChange={handleUserChange} showErrors={install.showErrors} errorMsg={install.errors.password} - /> + /> { onChange={handleUserChange} showErrors={install.showErrors} errorMsg={install.errors.confirmPassword} - /> + /> { !props.install.isLoading ? - - : - + + : + } {props.install.installRequest === 'FAILURE' &&
    Error: {props.install.installRequestError}
    } diff --git a/client/coral-admin/src/routes/Install/components/Steps/style.css b/client/coral-admin/src/routes/Install/components/Steps/style.css index dea752298..5880298fd 100644 --- a/client/coral-admin/src/routes/Install/components/Steps/style.css +++ b/client/coral-admin/src/routes/Install/components/Steps/style.css @@ -1,3 +1,8 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please remove this file + * when styles have been refactored. + */ + .step { padding: 20px 0; diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index f4a378e1d..9d9428bc4 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -68,7 +68,7 @@ class Comment extends React.Component { const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction'); const commentType = getCommentType(comment); - let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; + const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; const queryData = {root, comment, asset: comment.asset}; @@ -92,8 +92,8 @@ class Comment extends React.Component { { (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null + ?  ({t('comment.edited')}) + : null } {currentUserId !== comment.user.id && @@ -193,10 +193,10 @@ class Comment extends React.Component { /> {flagActions && flagActions.length ? + actions={flagActions} + actionSummaries={flagActionSummaries} + viewUserDetail={viewUserDetail} + /> : null} ); diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 3da727f99..3197ddd78 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -98,7 +98,7 @@ class ModerationQueue extends React.Component { rejectComment={props.rejectComment} currentAsset={props.currentAsset} currentUserId={this.props.currentUserId} - />; + />; }) } @@ -111,7 +111,7 @@ class ModerationQueue extends React.Component { + />
    ); } diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js index f19560549..7ac18f907 100644 --- a/client/coral-admin/src/routes/Moderation/components/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js @@ -8,7 +8,7 @@ const StorySearch = (props) => { const { root: { - assets = [] + assets, }, data: {loading} } = props; @@ -61,23 +61,23 @@ const StorySearch = (props) => { { loading - ? - : assets.map((story, i) => { - const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); + ? + : assets.nodes.map((story, i) => { + const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); - return ; - }) + return ; + }) } - {assets.length === 0 &&
    No results
    } + {assets.nodes.length === 0 &&
    No results
    }
    diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index 061e5351d..6f27e0b94 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -1,3 +1,8 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please remove this file + * when styles have been refactored. + */ + @custom-media --big-viewport (min-width: 780px); .listContainer { @@ -444,20 +449,20 @@ span { .commentLeave { opacity: 1.0; - transition: opacity 800ms; } .commentLeaveActive { opacity: 0; + transition: opacity 800ms; } .commentEnter { opacity: 0; - transition: opacity 800ms; } .commentEnterActive { opacity: 1.0; + transition: opacity 800ms; } .editedMarker { diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 722faaaa3..2ff6b6732 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -208,17 +208,22 @@ class ModerationContainer extends Component { // Still loading. return ; } - } else if(asset !== undefined || !('premodCount' in root)) { + } else if (asset !== undefined || !('premodCount' in root)) { // loading. return ; } - const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation); + const premodEnabled = assetId ? isPremod(asset.settings.moderation) : + isPremod(settings.moderation); + const currentQueueConfig = Object.assign({}, this.props.queueConfig); - if (premodEnabled) { + + if (premodEnabled && root.newCount === 0) { delete currentQueueConfig.new; - } else { + } + + if (!premodEnabled && root.premodCount === 0) { delete currentQueueConfig.premod; } @@ -355,8 +360,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql` variables: { asset_id: id, sortOrder: props.moderation.sortOrder, - allAssets: id === null - } + allAssets: id === null, + }, + fetchPolicy: 'network-only' }; }, }); diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js index 4175479e6..04aab2d84 100644 --- a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js @@ -89,23 +89,25 @@ class StorySearchContainer extends React.Component { export const withAssetSearchQuery = withQuery(gql` query SearchStories($value: String = "") { assets(query: {value: $value, limit: 10}) { - id - title - url - created_at - closedAt - author + nodes { + id + title + url + created_at + closedAt + author + } } } `, { - options: ({moderation: {storySearchString = ''}}) => { - return { - variables: { - value: storySearchString - } - }; - } -}); + options: ({moderation: {storySearchString = ''}}) => { + return { + variables: { + value: storySearchString + } + }; + } + }); export default compose( withRouter, diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index 28ca259d8..866218d73 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -69,15 +69,15 @@ function getCommentQueues(comment, queueConfig) { const queues = []; Object.keys(queueConfig).forEach((key) => { const {action_type, statuses, tags} = queueConfig[key]; - let addToQueues = false; - if (statuses && statuses.indexOf(comment.status) >= 0) { - addToQueues = true; + let addToQueues = true; + if (statuses && statuses.indexOf(comment.status) === -1) { + addToQueues = false; } - if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) { - addToQueues = true; + if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) { + addToQueues = false; } - if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) { - addToQueues = true; + if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) { + addToQueues = false; } if (addToQueues) { queues.push(key); diff --git a/client/coral-admin/src/routes/Moderation/queueConfig.js b/client/coral-admin/src/routes/Moderation/queueConfig.js index 9a23ccc6c..86a622177 100644 --- a/client/coral-admin/src/routes/Moderation/queueConfig.js +++ b/client/coral-admin/src/routes/Moderation/queueConfig.js @@ -1,16 +1,16 @@ import t from 'coral-framework/services/i18n'; export default { + new: { + statuses: ['NONE'], + icon: 'question_answer', + name: t('modqueue.new'), + }, premod: { statuses: ['PREMOD'], icon: 'access_time', name: t('modqueue.premod'), }, - new: { - statuses: ['NONE', 'PREMOD'], - icon: 'question_answer', - name: t('modqueue.new'), - }, reported: { action_type: 'FLAG', statuses: ['NONE', 'PREMOD'], diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 7f9098fba..d57b8f779 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -50,17 +50,22 @@ export default class Stories extends Component { return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } - onStatusClick = (closeStream, id, statusMenuOpen) => () => { + onStatusClick = (closeStream, id, statusMenuOpen) => async () => { if (statusMenuOpen) { this.setState((prev) => { prev.statusMenus[id] = false; return prev; }); - this.props.updateAssetState(id, closeStream ? Date.now() : null) - .then(() => { - const {search, sort, filter, page} = this.state; - this.props.fetchAssets(page, limit, search, sort, filter); - }); + + try { + await this.props.updateAssetState(id, closeStream ? Date.now() : null); + const {search, sort, filter, page} = this.state; + this.props.fetchAssets(page, limit, search, sort, filter); + } catch (err) { + + // TODO: handle error. + console.error(err); + } } else { this.setState((prev) => { prev.statusMenus[id] = true; @@ -134,20 +139,20 @@ export default class Stories extends Component { {t('streams.closed')}
    {t('streams.sort_by')}
    - - {t('streams.newest')} - {t('streams.oldest')} - - + + {t('streams.newest')} + {t('streams.oldest')} + + { assetsIds.length - ?
    + ?
    {t('streams.article')} @@ -162,7 +167,7 @@ export default class Stories extends Component { page={this.state.page} onNewPageHandler={this.onPageClick} />
    - : {t('streams.empty_result')} + : {t('streams.empty_result')} }
    ); diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index 726b54e19..ec5a43c15 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -56,13 +56,13 @@ export default ({handleChange, handleApply, changed, ...props}) => ( title: t('configure.enable_questionbox'), description: t('configure.enable_questionbox_description') }} /> - { - props.questionBoxEnable && - } + { + props.questionBoxEnable && + } diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index 8ccc6ba31..285df40ed 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -41,39 +41,39 @@ class QuestionBoxBuilder extends React.Component {
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'default'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="default" >
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'forum'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="forum" >
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'build'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="build" >
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="format_quote" >
    diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 2d4a16795..c009bbb76 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -115,7 +115,7 @@ class ConfigureStreamContainer extends Component { />

    {closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}

    - {(closedAt === 'open' && closedTimeout) ?

    {t('configure.comment_stream_will_close')} {this.getClosedIn()}.

    : ''} + {(closedAt === 'open' && closedTimeout) ?

    {t('configure.comment_stream_will_close')} {this.getClosedIn()}.

    : ''} {view.map((comment) => { - return commentIsIgnored(comment) - ? - : ; + return ( + + ); })} u.id === comment.user.id) + ); + } + hasIgnoredReplies() { return this.props.comment.replies && - this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply)); + this.props.comment.replies.nodes.some((reply) => this.commentIsIgnored(reply)); } loadNewReplies = () => { @@ -295,14 +296,12 @@ export default class Comment extends React.Component { this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout); } - // if still in the edit window, set a timeout to re-render once it expires - const msLeftToEdit = editWindowRemainingMs(this.props.comment); - if (msLeftToEdit > 0) { + // if still in the edit window, set a timeout to handle expiration. + if (this.state.isEditable) { + const msLeftToEdit = editWindowRemainingMs(this.props.comment); this.editWindowExpiryTimeout = setTimeout(() => { - - // re-render - this.setState(this.state); - }, msLeftToEdit); + this.setState({isEditable: false}); + }, Math.max(msLeftToEdit, 0)); } } componentWillUnmount() { @@ -320,7 +319,6 @@ export default class Comment extends React.Component { comment, postFlag, parentId, - ignoreUser, highlighted, postComment, currentUser, @@ -335,15 +333,18 @@ export default class Comment extends React.Component { charCountEnable, showSignInDialog, liveUpdates, - commentIsIgnored, animateEnter, emit, commentClassNames = [] } = this.props; + if (this.commentIsIgnored(comment)) { + return ; + } + const view = this.getVisibileReplies(); - // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). + // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). const isActive = isCommentActive(comment.status); const {loadingState} = this.state; @@ -447,12 +448,20 @@ export default class Comment extends React.Component { {isStaff(comment.tags) ? Staff : null} + + { (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null + ?  ({t('comment.edited')}) + : null } @@ -468,23 +477,13 @@ export default class Comment extends React.Component { /* User can edit/delete their own comment for a short window after posting */ { - commentIsStillEditable(comment) && + this.state.isEditable && Edit } } - { isActive && (currentUser && (comment.user.id !== currentUser.id)) && - - /* TopRightMenu allows currentUser to ignore other users' comments */ - - - - } { !isActive && } @@ -492,7 +491,7 @@ export default class Comment extends React.Component {
    { this.state.isEditing - ? - :
    - + :
    +
    }
    @@ -562,23 +561,22 @@ export default class Comment extends React.Component { {activeReplyBox === comment.id ? + commentPostedHandler={this.commentPostedHandler} + charCountEnable={charCountEnable} + maxCharCount={maxCharCount} + setActiveReplyBox={setActiveReplyBox} + parentId={(depth < THREADING_LEVEL) ? comment.id : parentId} + notify={notify} + postComment={postComment} + currentUser={currentUser} + assetId={asset.id} + /> : null} - {view.map((reply) => { - return commentIsIgnored(reply) - ? - : { + return ( + ; - })} + /> + ); + })}
    { this.isEditWindowExpired() - ? + ? {t('edit_comment.edit_window_expired')} { typeof this.props.stopEditing === 'function' - ?  {t('edit_comment.edit_window_expired_close')} - : null + ?  {t('edit_comment.edit_window_expired_close')} + : null } - : + : {t('edit_comment.edit_window_timer_prefix')} { return
    { count ? - - : null + + : null }
    ; }; diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 6959ff662..89e7b13ae 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n'; import CommentBox from 'talk-plugin-commentbox/CommentBox'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; import {isCommentActive} from 'coral-framework/utils'; -import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui'; +import {Button, Tab, TabCount, TabPane} from 'coral-ui'; import cn from 'classnames'; import {getTopLevelParent, attachCommentToParent} from '../graphql/utils'; @@ -23,8 +23,6 @@ import StreamTabPanel from '../containers/StreamTabPanel'; import styles from './Stream.css'; -const SpinnerWhenLoading = ({loading, children}) => loading ? :
    {children}
    ; - class Stream extends React.Component { constructor(props) { @@ -42,15 +40,6 @@ class Stream extends React.Component { } } - commentIsIgnored = (comment) => { - const me = this.props.root.me; - return ( - me && - me.ignoredUsers && - me.ignoredUsers.find((u) => u.id === comment.user.id) - ); - }; - renderHighlightedComment() { const { data, @@ -66,7 +55,6 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - ignoreUser, loadNewReplies, auth: {user}, emit, @@ -92,7 +80,6 @@ class Stream extends React.Component { data={data} root={root} commentClassNames={commentClassNames} - ignoreUser={ignoreUser} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} @@ -108,7 +95,6 @@ class Stream extends React.Component { deleteAction={deleteAction} showSignInDialog={showSignInDialog} key={topLevelComment.id} - commentIsIgnored={this.commentIsIgnored} comment={topLevelComment} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} @@ -135,7 +121,6 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - ignoreUser, activeStreamTab, setActiveStreamTab, loadNewReplies, @@ -144,6 +129,7 @@ class Stream extends React.Component { emit, sortOrder, sortBy, + loading, } = this.props; const slotProps = {data}; @@ -171,6 +157,7 @@ class Stream extends React.Component { tabPaneSlot={'streamTabPanes'} slotProps={slotProps} queryData={slotQueryData} + loading={loading} appendTabs={ All Comments {totalCommentCount} @@ -183,7 +170,6 @@ class Stream extends React.Component { root={root} comments={comments} commentClassNames={commentClassNames} - ignoreUser={ignoreUser} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} @@ -197,7 +183,6 @@ class Stream extends React.Component { loadNewReplies={loadNewReplies} deleteAction={deleteAction} showSignInDialog={showSignInDialog} - commentIsIgnored={this.commentIsIgnored} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} @@ -223,7 +208,6 @@ class Stream extends React.Component { viewAllComments, auth: {loggedIn, user}, editName, - loading, } = this.props; const {keepCommentBox} = this.state; const open = !asset.isClosed; @@ -257,16 +241,16 @@ class Stream extends React.Component { {open ?
    - - - {!banned && + + + {!banned && temporarilySuspended && {t( @@ -275,13 +259,13 @@ class Stream extends React.Component { timeago(user.suspension.until) )} } - {banned && + {banned && } - {showCommentBox && + {showCommentBox && } -
    +
    :

    {asset.settings.closedMessage}

    } )} - - {highlightedComment - ? this.renderHighlightedComment() - : this.renderTabPanel() - } - + {highlightedComment + ? this.renderHighlightedComment() + : this.renderTabPanel() + }
    ); } @@ -325,9 +307,6 @@ Stream.propTypes = { notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, - // dispatch action to ignore another user - ignoreUser: React.PropTypes.func, - // edit a comment, passed (id, asset_id, { body }) editComment: React.PropTypes.func }; diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.css b/client/coral-embed-stream/src/components/StreamTabPanel.css new file mode 100644 index 000000000..9398d373c --- /dev/null +++ b/client/coral-embed-stream/src/components/StreamTabPanel.css @@ -0,0 +1,3 @@ +.spinnerContainer { + margin-top: 16px; +} diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.js b/client/coral-embed-stream/src/components/StreamTabPanel.js index 0f2092092..50f54c2a0 100644 --- a/client/coral-embed-stream/src/components/StreamTabPanel.js +++ b/client/coral-embed-stream/src/components/StreamTabPanel.js @@ -1,19 +1,23 @@ import React from 'react'; -import {TabBar, TabContent} from 'coral-ui'; +import {Spinner, TabBar, TabContent} from 'coral-ui'; import PropTypes from 'prop-types'; +import styles from './StreamTabPanel.css'; class StreamTabPanel extends React.Component { render() { - const {activeTab, setActiveTab, tabs, tabPanes, sub} = this.props; + const {activeTab, setActiveTab, tabs, tabPanes, sub, loading} = this.props; return (
    {tabs} - - {tabPanes} - + {loading + ?
    + : + {tabPanes} + + }
    ); } diff --git a/client/coral-embed-stream/src/components/SuspendedAccount.js b/client/coral-embed-stream/src/components/SuspendedAccount.js index 7c0b5933c..903b1bcf4 100644 --- a/client/coral-embed-stream/src/components/SuspendedAccount.js +++ b/client/coral-embed-stream/src/components/SuspendedAccount.js @@ -45,40 +45,40 @@ class SuspendedAccount extends Component { return { - canEditName ? + canEditName ? t('framework.edit_name.msg') : {t('framework.banned_account_header')}
    {t('framework.banned_account_body')}
    - }
    + } { canEditName ? -
    -
    - {alert} -
    - - this.setState({username: e.target.value})} - rows={3}/>
    - -
    : null +
    +
    + {alert} +
    + + this.setState({username: e.target.value})} + rows={3}/>
    + +
    : null }
    ; } diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js index b0ea55c08..fd4587356 100644 --- a/client/coral-embed-stream/src/components/TopRightMenu.js +++ b/client/coral-embed-stream/src/components/TopRightMenu.js @@ -52,7 +52,7 @@ export class TopRightMenu extends React.Component { user={comment.user} cancel={reset} ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError} - /> + /> ); diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 6ddba46f8..733708a97 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -16,7 +16,8 @@ const slots = [ 'commentContent', 'commentReactions', 'commentAvatar', - 'commentAuthorName' + 'commentAuthorName', + 'commentAuthorTags' ]; /** @@ -84,13 +85,24 @@ const singleCommentFragment = gql` edited editableUntil } + ${getSlotFragmentSpreads(slots, 'comment')} } `; const withCommentFragments = withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { + me { + ignoredUsers { + id + } + } __typename + me { + ignoredUsers { + id + } + } ${getSlotFragmentSpreads(slots, 'root')} } `, @@ -115,7 +127,6 @@ const withCommentFragments = withFragments({ endCursor } `, THREADING_LEVEL)} - ${getSlotFragmentSpreads(slots, 'comment')} } ${singleCommentFragment} ` diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 271421dea..273d39589 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream'; import { withPostComment, withPostFlag, withPostDontAgree, - withDeleteAction, withIgnoreUser, withEditComment + withDeleteAction, withEditComment } from 'coral-framework/graphql/mutations'; import * as authActions from 'coral-embed-stream/src/actions/auth'; @@ -84,6 +84,10 @@ class StreamContainer extends React.Component { return prev; } + // Newest top-level comments are only added when sorting by 'newest first'. + if (!commentAdded.parent && !this.isSortedByNewestFirst()) { + return prev; + } return insertCommentIntoEmbedQuery(prev, commentAdded); }, }); @@ -163,32 +167,9 @@ class StreamContainer extends React.Component { } componentWillReceiveProps(nextProps) { - const prevSortedNewest = this.isSortedByNewestFirst(this.props); - const nextSortedNewest = this.isSortedByNewestFirst(nextProps); - - // When switching to 'Newest first' we refetch and subscribe so that - // we always have the newest comments. - if (!prevSortedNewest && nextSortedNewest) { + if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) { nextProps.data.refetch(); - this.subscribeToCommentsAdded(); } - - // When switching away from 'Newest first' unsubscribe from newest comments. - if (prevSortedNewest && !nextSortedNewest) { - this.unsubscribeCommentsAdded(); - } - } - - shouldComponentUpdate(nextProps) { - const prevSortedNewest = this.isSortedByNewestFirst(this.props); - const nextSortedNewest = this.isSortedByNewestFirst(nextProps); - if (!prevSortedNewest && nextSortedNewest) { - - // When switching to 'Newest first' we refetch => skip - // rendering this frame and wait for refetch to kick in. - return false; - } - return true; } userIsDegraged({auth: {user}} = this.props) { @@ -321,7 +302,7 @@ const fragments = { questionBoxEnable questionBoxContent questionBoxIcon - closeTimeout + closedTimeout closedMessage charCountEnable charCount @@ -393,7 +374,6 @@ export default compose( withPostComment, withPostFlag, withPostDontAgree, - withIgnoreUser, withDeleteAction, withEditComment, )(StreamContainer); diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js index 152f34e39..fde6e3be4 100644 --- a/client/coral-embed-stream/src/containers/StreamTabPanel.js +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -86,6 +86,7 @@ class StreamTabPanelContainer extends React.Component { setActiveTab={this.props.setActiveTab} tabs={this.getPluginTabElements().concat(this.props.appendTabs)} tabPanes={this.getPluginTabPaneElements().concat(this.props.appendTabPanes)} + loading={this.props.loading} sub={this.props.sub} /> ); @@ -110,6 +111,7 @@ StreamTabPanelContainer.propTypes = { queryData: PropTypes.object, className: PropTypes.string, sub: PropTypes.bool, + loading: PropTypes.bool, }; const mapStateToProps = (state) => ({ diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 9ec9a2132..09470de14 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -108,33 +108,6 @@ export default { `, }, mutations: { - IgnoreUser: ({variables}) => ({ - updateQueries: { - CoralEmbedStream_Embed: (previousData) => { - const ignoredUserId = variables.id; - const updated = update(previousData, {me: {ignoredUsers: {$push: [{ - id: ignoredUserId, - __typename: 'User', - }]}}}); - return updated; - } - } - }), - StopIgnoringUser: ({variables}) => ({ - updateQueries: { - CoralEmbedStream_Profile: (previousData) => { - const noLongerIgnoredUserId = variables.id; - - // remove noLongerIgnoredUserId from ignoredUsers - const updated = update(previousData, {me: {ignoredUsers: { - $apply: (ignoredUsers) => { - return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId); - } - }}}); - return updated; - } - } - }), PostComment: ({ variables: {comment: {asset_id, body, parent_id, tags = []}}, state: {auth}, diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index ac13382bc..0c46b45e3 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -60,7 +60,7 @@ function findAndInsertComment(parent, comment) { [connectionField]: { nodes: { $apply: (nodes) => - nodes.map((node) => findAndInsertComment(node, comment)) + nodes.map((node) => findAndInsertComment(node, comment)) }, }, }); @@ -176,7 +176,7 @@ function findAndInsertFetchedComments(parent, comments, parent_id) { [connectionField]: { nodes: { $apply: (nodes) => - nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id)) + nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id)) }, }, }); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 010a65ab1..a285d1ff6 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -1,3 +1,8 @@ +/** + * @TODO: deprecated as default styles should be included in the components. + * Please remove this file when styles have been refactored. + */ + * { font-family: inherit; font-style: inherit; diff --git a/client/coral-embed/src/Snackbar.js b/client/coral-embed/src/Snackbar.js index c518288c6..11219102d 100644 --- a/client/coral-embed/src/Snackbar.js +++ b/client/coral-embed/src/Snackbar.js @@ -38,7 +38,7 @@ export default class Snackbar { } alert(message) { - const [type, text] = message.split('|'); + const {type, text} = JSON.parse(message); this.el.style.transform = 'translate(-50%, 20px)'; this.el.style.opacity = 0; this.el.className = `coral-notif-${type}`; diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index 557ee3b3d..72dc8c988 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Children} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; @@ -28,18 +28,13 @@ class IfSlotIsEmpty extends React.Component { } render() { - const {className, component: Component = 'div', children} = this.props; - return ( - - {this.isSlotEmpty() ? children : null} - - ); + const {children} = this.props; + return this.isSlotEmpty() ? Children.only(children) : null; } } IfSlotIsEmpty.propTypes = { slot: PropTypes.string, - className: PropTypes.string, }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 4e3abfae4..46e68fe60 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Children} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; @@ -28,18 +28,13 @@ class IfSlotIsNotEmpty extends React.Component { } render() { - const {className, component: Component = 'div', children} = this.props; - return ( - - {this.isSlotEmpty() ? null : children} - - ); + const {children} = this.props; + return this.isSlotEmpty() ? null : Children.only(children); } } IfSlotIsNotEmpty.propTypes = { slot: PropTypes.string, - className: PropTypes.string, }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/components/Popup.js b/client/coral-framework/components/Popup.js index 1fe3408ea..33041442f 100644 --- a/client/coral-framework/components/Popup.js +++ b/client/coral-framework/components/Popup.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; export default class Popup extends Component { ref = null; detectCloseInterval = null; + resetCallbackInterval = null; constructor(props) { super(props); @@ -41,16 +42,26 @@ export default class Popup extends Component { this.ref.onunload = () => { this.onUnload(); - const interval = setInterval(() => { + if (this.resetCallbackInterval) { + clearInterval(this.resetCallbackInterval); + } + + this.resetCallbackInterval = setInterval(() => { if (this.ref && this.ref.onload === null) { + clearInterval(this.resetCallbackInterval); + this.resetCallbackInterval = null; this.setCallbacks(); - clearInterval(interval); } }, 50); + if (this.detectCloseInterval) { + clearInterval(this.detectCloseInterval); + } + this.detectCloseInterval = setInterval(() => { if (!this.ref || this.ref.closed) { clearInterval(this.detectCloseInterval); + this.detectCloseInterval = null; this.onClose(); } }, 50); diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 91d2ae0ab..4254400bb 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation( } `, { props: ({mutate}) => ({ - ignoreUser: ({id}) => { + ignoreUser: (id) => { return mutate({ variables: { id, diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js index dd36ae07f..ef64a2c84 100644 --- a/client/coral-framework/helpers/validate.js +++ b/client/coral-framework/helpers/validate.js @@ -1,5 +1,5 @@ export default { - email: (email) => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)), + email: (email) => (/^.+@.+\..+$/.test(email)), password: (pass) => (/^(?=.{8,}).*$/.test(pass)), confirmPassword: () => true, username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)), diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 46ec19eb7..f16809322 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -84,8 +84,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const updateCallbacks = [base.update || config.options.update] - .concat(...configs.map((cfg) => cfg.update)) - .filter((i) => i); + .concat(...configs.map((cfg) => cfg.update)) + .filter((i) => i); const update = (proxy, result) => { if (getResponseErrors(result)) { @@ -101,28 +101,28 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { base.updateQueries || config.options.updateQueries, ...configs.map((cfg) => cfg.updateQueries) ] - .filter((i) => i) - .reduce((res, map) => { - Object.keys(map).forEach((key) => { - if (!(key in res)) { - res[key] = (prev, result) => { - if (getResponseErrors(result.mutationResult)) { + .filter((i) => i) + .reduce((res, map) => { + Object.keys(map).forEach((key) => { + if (!(key in res)) { + res[key] = (prev, result) => { + if (getResponseErrors(result.mutationResult)) { // Do not run updates when we have mutation errors. - return prev; - } - return map[key](prev, result) || prev; - }; - } else { - const existing = res[key]; - res[key] = (prev, result) => { - const next = existing(prev, result); - return map[key](next, result) || next; - }; - } - }); - return res; - }, {}); + return prev; + } + return map[key](prev, result) || prev; + }; + } else { + const existing = res[key]; + res[key] = (prev, result) => { + const next = existing(prev, result); + return map[key](next, result) || next; + }; + } + }); + return res; + }, {}); const wrappedConfig = { variables, diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index c0825f28b..9548827ac 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -69,7 +69,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const status = networkStatusToString(networkStatus); const {root} = separateDataAndRoot(data); - this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, data: root}); + this.context.eventEmitter.emit(`query.${this.name}.${status}`, {variables, data: root}); } nextData(data) { @@ -121,7 +121,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const resolvedDocument = this.resolveDocument(lmArgs.query); const fetchName = getDefinitionName(resolvedDocument); this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.begin`, + `query.${this.name}.fetchMore.${fetchName}.begin`, {variables: lmArgs.variables}); // Resolve document fragments before passing it to `apollo-client`. @@ -129,18 +129,18 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { ...lmArgs, query: resolvedDocument, }) - .then((res) => { - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.success`, - {variables: lmArgs.variables, data: res.data}); - return Promise.resolve(res); - }) - .catch((err) => { - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.error`, - {variables: lmArgs.variables, error: err}); - throw err; - }); + .then((res) => { + this.context.eventEmitter.emit( + `query.${this.name}.fetchMore.${fetchName}.success`, + {variables: lmArgs.variables, data: res.data}); + return Promise.resolve(res); + }) + .catch((err) => { + this.context.eventEmitter.emit( + `query.${this.name}.fetchMore.${fetchName}.error`, + {variables: lmArgs.variables, error: err}); + throw err; + }); }, }; } @@ -168,11 +168,11 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const base = (typeof this.wrappedConfig.options === 'function') ? this.wrappedConfig.options(data) : this.wrappedConfig.options; - const configs = this.graphqlRegistry.getQueryOptions(name); + const configs = this.graphqlRegistry.getQueryOptions(this.name); const reducerCallbacks = [base.reducer || ((i) => i)] - .concat(...configs.map((cfg) => cfg.reducer)) - .filter((i) => i); + .concat(...configs.map((cfg) => cfg.reducer)) + .filter((i) => i); const reducer = withSkipOnErrors( reducerCallbacks.reduce( diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index ac94d8b29..a3ae5b18e 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -15,6 +15,25 @@ import globalFragments from 'coral-framework/graphql/fragments'; import {createStorage} from 'coral-framework/services/storage'; import {createHistory} from 'coral-framework/services/history'; +/** + * getStaticConfiguration will return a singleton of the static configuration + * object provided via a JSON DOM element. + */ +const getStaticConfiguration = (() => { + let staticConfiguration = null; + return () => { + if (staticConfiguration != null) { + return staticConfiguration; + } + + const configElement = document.querySelector('#data'); + + staticConfiguration = JSON.parse(configElement ? configElement.textContent : '{}'); + + return staticConfiguration; + }; +})(); + /** * getAuthToken returns the active auth token or null * Note: this method does not have access to the cookie based token used by @@ -49,7 +68,6 @@ const getAuthToken = (store, storage) => { * @return {Object} context */ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) { - const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); const storage = createStorage(); const history = createHistory(BASE_PATH); @@ -63,13 +81,28 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. return getAuthToken(store, storage); }; + const rest = createRestClient({ uri: `${BASE_PATH}api/v1`, token, }); + + // Try to get an overrided liveUri from the static config, if none is found, + // build it. + let {LIVE_URI: liveUri} = getStaticConfiguration(); + if (liveUri == null) { + + // The protocol must match the origin protocol, secure/insecure. + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + + // Compose the live url from this protocol, the current host + base path + // with the live path appended to it. + liveUri = `${protocol}://${location.host}${BASE_PATH}api/v1/live`; + } + const client = createClient({ uri: `${BASE_PATH}api/v1/graph/ql`, - liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, + liveUri, token, }); const plugins = createPluginsService(pluginsConfig); @@ -79,6 +112,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // Use default notification service (pym based) notification = createNotificationService(pym); } + const context = { client, pym, diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js index 5730200ad..a3f1a6520 100644 --- a/client/coral-framework/services/notification.js +++ b/client/coral-framework/services/notification.js @@ -5,14 +5,14 @@ */ export function createNotificationService(pym) { return { - success(msg) { - pym.sendMessage('coral-alert', `success|${msg}`); + success(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'success', text})); }, - error(msg) { - pym.sendMessage('coral-alert', `error|${msg}`); + error(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'error', text})); }, - info(msg) { - pym.sendMessage('coral-alert', `info|${msg}`); + info(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'info', text})); }, }; } diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index 1aae0c662..301428c12 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -81,11 +81,11 @@ class PluginsService { const pluginConfig = reduxState.config.plugin_config || emptyConfig; return flatten(this.plugins - // Filter out components that have slots and have been disabled in `plugin_config` - .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) + // Filter out components that have slots and have been disabled in `plugin_config` + .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) - .filter((o) => o.module.slots[slot]) - .map((o) => o.module.slots[slot]) + .filter((o) => o.module.slots[slot]) + .map((o) => o.module.slots[slot]) ) .filter((component) => { if(!component.isExcluded) { @@ -114,8 +114,8 @@ class PluginsService { config: pluginConfig, ...( component.fragments - ? pick(queryData, Object.keys(component.fragments)) - : withWarnings(component, queryData) + ? pick(queryData, Object.keys(component.fragments)) + : withWarnings(component, queryData) ) }; } diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 533ed85db..4424e62c5 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -26,7 +26,7 @@ export const getMyActionSummary = (type, comment) => { .find((a) => a.current_user); }; - /** +/** * getActionSummary * retrieves the action summaries based on the type and the comment * array could be length > 1, as in the case of FlagActionSummary @@ -184,3 +184,16 @@ export function getShallowChanges(a, b) { return union(Object.keys(a), Object.keys(b)) .filter((key) => a[key] !== b[key]); } + +// TODO: replace with something less fragile. +// NOT_REACTION_TYPES are the action summaries that are not reactions. +const NOT_REACTION_TYPES = [ + 'FlagActionSummary', + 'DontAgreeActionSummary', +]; + +export function getTotalReactionsCount(actionSummaries) { + return actionSummaries + .filter(({__typename}) => !NOT_REACTION_TYPES.includes(__typename)) + .reduce((total, {count}) => total + count, 0); +} diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index 044583026..ccdf8ba42 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -1,4 +1,4 @@ - /** +/** * getReliability * retrieves reliability value as string */ diff --git a/client/coral-settings/components/IgnoredUsers.css b/client/coral-settings/components/IgnoredUsers.css deleted file mode 100644 index 716140256..000000000 --- a/client/coral-settings/components/IgnoredUsers.css +++ /dev/null @@ -1,24 +0,0 @@ -.ignoredUser { - display: table-row; -} - -.ignoredUserList { - display: table; -} - -.ignoredUser > * { - display: table-cell; -} - -.stopListening { - color: #D0011B; -} - -.link { - text-decoration: underline; - cursor: pointer; -} - -.stopListening:before { - content: '\00a0\00a0\00a0\00a0'; -} \ No newline at end of file diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index f5ea64005..aab0e2a6a 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -3,12 +3,10 @@ import {compose, gql} from 'react-apollo'; import React, {Component} from 'react'; import {bindActionCreators} from 'redux'; import {withQuery} from 'coral-framework/hocs'; - -import {withStopIgnoringUser} from 'coral-framework/graphql/mutations'; +import Slot from 'coral-framework/components/Slot'; import {link} from 'coral-framework/services/pym'; import NotLoggedIn from '../components/NotLoggedIn'; -import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'talk-plugin-history/CommentHistory'; @@ -55,7 +53,7 @@ class ProfileContainer extends Component { }; render() { - const {auth, auth: {user}, showSignInDialog, stopIgnoringUser, root, data} = this.props; + const {auth, auth: {user}, showSignInDialog, root, data} = this.props; const {me} = this.props.root; const loading = this.props.data.loading; @@ -77,15 +75,11 @@ class ProfileContainer extends Component {

    {user.username}

    {emailAddress ?

    {emailAddress}

    : null} - {me.ignoredUsers && me.ignoredUsers.length - ?
    -

    {t('framework.ignored_users')}

    - -
    - : null} +
    @@ -98,8 +92,10 @@ class ProfileContainer extends Component { } } -// TODO: This Slot should be included in `talk-plugin-history` instead. const slots = [ + 'profileSections', + + // TODO: This Slot should be included in `talk-plugin-history` instead. 'commentContent', ]; @@ -108,6 +104,11 @@ const CommentFragment = gql` nodes { id body + replyCount + action_summaries { + count + __typename + } asset { id title @@ -138,10 +139,6 @@ const withProfileQuery = withQuery( query CoralEmbedStream_Profile { me { id - ignoredUsers { - id, - username, - } comments(query: {limit: 10}) { ...TalkSettings_CommentConnectionFragment } @@ -160,6 +157,5 @@ const mapDispatchToProps = (dispatch) => export default compose( connect(mapStateToProps, mapDispatchToProps), - withStopIgnoringUser, withProfileQuery )(ProfileContainer); diff --git a/client/coral-ui/components/CoralLogo.js b/client/coral-ui/components/CoralLogo.js index fe4706429..ba640020e 100644 --- a/client/coral-ui/components/CoralLogo.js +++ b/client/coral-ui/components/CoralLogo.js @@ -4,34 +4,34 @@ import styles from './CoralLogo.css'; const CoralLogo = ({className = ''}) => ( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - + ); CoralLogo.propTypes = { diff --git a/client/coral-ui/components/Dialog.js b/client/coral-ui/components/Dialog.js index 98bf92074..93431db4c 100644 --- a/client/coral-ui/components/Dialog.js +++ b/client/coral-ui/components/Dialog.js @@ -57,7 +57,7 @@ export default class Dialog extends Component { className={`mdl-dialog ${className}`} {...rest} > - {children} + {children} ); } diff --git a/client/coral-ui/components/Pager.js b/client/coral-ui/components/Pager.js index 2383d9171..8f560593e 100644 --- a/client/coral-ui/components/Pager.js +++ b/client/coral-ui/components/Pager.js @@ -4,7 +4,7 @@ import styles from './Pager.css'; const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
  • onClickHandler(i + 1)}> + key={i} onClick={() => onClickHandler(i + 1)}> {i + 1}
  • ); @@ -18,18 +18,18 @@ const Pager = ({totalPages, page, onNewPageHandler}) => ( onClick={() => onNewPageHandler(page - 1)}> Prev - : + : null } {Rows(page, totalPages, onNewPageHandler)} { (page < totalPages && totalPages > 1) ? -
  • onNewPageHandler(page + 1)}> +
  • onNewPageHandler(page + 1)}> Next -
  • - : - null + + : + null } diff --git a/client/coral-ui/components/Wizard.js b/client/coral-ui/components/Wizard.js index d75f30927..97b056674 100644 --- a/client/coral-ui/components/Wizard.js +++ b/client/coral-ui/components/Wizard.js @@ -8,12 +8,12 @@ const Wizard = (props) => { {React.Children.toArray(children) .filter((child, i) => i === currentStep) .map((child, i) => - React.cloneElement(child, { - i, - currentStep, - ...rest - }) - )} + React.cloneElement(child, { + i, + currentStep, + ...rest + }) + )} ); }; diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index bf3da6f01..a225bda41 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -93,18 +93,24 @@ export default class FlagButton extends Component { }; if (reason === 'COMMENT_NOAGREE') { postDontAgree(action) - .then(({data}) => { - if (itemType === 'COMMENTS') { - this.setState({localPost: data.createDontAgree.dontagree.id}); - } - }); + .then(({data}) => { + if (itemType === 'COMMENTS') { + this.setState({localPost: data.createDontAgree.dontagree.id}); + } + }) + .catch((err) => { + console.error(err); + }); } else { postFlag({...action, reason}) - .then(({data}) => { - if (itemType === 'COMMENTS') { - this.setState({localPost: data.createFlag.flag.id}); - } - }); + .then(({data}) => { + if (itemType === 'COMMENTS') { + this.setState({localPost: data.createFlag.flag.id}); + } + }) + .catch((err) => { + console.error(err); + }); } } } @@ -153,15 +159,15 @@ export default class FlagButton extends Component { className={cn(`${name}-button`, {[`${name}-button-flagged`]: flagged}, styles.button)}> { flagged - ? {t('reported')} - : {t('report')} + ? {t('reported')} + : {t('report')} } flag + aria-hidden={true}>flag { this.state.showMenu && @@ -190,10 +196,10 @@ export default class FlagButton extends Component { } { this.state.reason &&
    -
    -
    onHeaderClickHandler({field: header.field})}> + key={i} + className="mdl-data-table__cell--non-numeric" + onClick={() => onHeaderClickHandler({field: header.field})}> {header.title}