From eb4d85fb86823c9554781725b0975eed071a5b99 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 26 Jun 2017 17:34:15 +0700 Subject: [PATCH 01/21] Refactor TagsInput and Improve UX --- .../coral-admin/src/components/TagsInput.css | 51 ++++++++++++++++++ .../coral-admin/src/components/TagsInput.js | 31 +++++++++++ .../routes/Configure/components/Configure.css | 7 --- .../routes/Configure/components/Domainlist.js | 4 +- .../routes/Configure/components/Wordlist.js | 6 +-- .../components/Steps/PermittedDomainsStep.js | 5 +- package.json | 3 +- views/admin.ejs | 52 ------------------- yarn.lock | 15 ++++-- 9 files changed, 98 insertions(+), 76 deletions(-) create mode 100644 client/coral-admin/src/components/TagsInput.css create mode 100644 client/coral-admin/src/components/TagsInput.js diff --git a/client/coral-admin/src/components/TagsInput.css b/client/coral-admin/src/components/TagsInput.css new file mode 100644 index 000000000..6bb630521 --- /dev/null +++ b/client/coral-admin/src/components/TagsInput.css @@ -0,0 +1,51 @@ +.root { + background-color: #fff; + border: 1px solid #ccc; + overflow: hidden; + padding-left: 5px; + padding-top: 5px; + +} + +.rootFocuesd { + border-color: rgb(142, 76, 65); +} + +.tag { + background-color: rgb(255, 220, 214); + border-radius: 2px; + border: 1px solid rgb(244, 126, 107); + color: rgb(244, 126, 107); + display: inline-block; + font-family: sans-serif; + font-size: 13px; + font-weight: 400; + margin-bottom: 5px; + margin-right: 5px; + padding: 5px; +} + +.tagRemove { + cursor: pointer; + font-weight: bold; + color: rgb(101, 24, 23); + + &::before { + content: " ×"; + } +} + +.input > input { + background: transparent; + border: 0; + color: #777; + font-family: sans-serif; + font-size: 13px; + font-weight: 400; + margin-bottom: 6px; + margin-top: 1px; + outline: none; + padding: 5px; +} + + diff --git a/client/coral-admin/src/components/TagsInput.js b/client/coral-admin/src/components/TagsInput.js new file mode 100644 index 000000000..d9854ed86 --- /dev/null +++ b/client/coral-admin/src/components/TagsInput.js @@ -0,0 +1,31 @@ +import React from 'react'; +import TagsInput from 'react-tagsinput'; +import styles from './TagsInput.css'; +import AutosizeInput from 'react-input-autosize'; + +const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) => + ; + +export default (props) => { + return ( + data.split(',').map((d) => d.trim())} + className={styles.root} + focusedClassName={styles.rootFocus} + renderInput={autosizingRenderInput} + {...props} + + tagProps={{ + className: styles.tag, + classNameRemove: styles.tagRemove, + ...props.tagProps, + }} + inputProps={{ + className: styles.input, + ...props.inputProps, + }} + /> + ); +}; diff --git a/client/coral-admin/src/routes/Configure/components/Configure.css b/client/coral-admin/src/routes/Configure/components/Configure.css index 22e46518c..5b02b963a 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.css +++ b/client/coral-admin/src/routes/Configure/components/Configure.css @@ -136,13 +136,6 @@ resize: none; } -#bannedWordlist, #suspectWordlist { - display: block; - input { - width: 100%; - } -} - .customCSSInput { width: 100%; font-size: 14px; diff --git a/client/coral-admin/src/routes/Configure/components/Domainlist.js b/client/coral-admin/src/routes/Configure/components/Domainlist.js index 45790ec89..7b7565c5e 100644 --- a/client/coral-admin/src/routes/Configure/components/Domainlist.js +++ b/client/coral-admin/src/routes/Configure/components/Domainlist.js @@ -1,7 +1,7 @@ import React from 'react'; import {Card} from 'coral-ui'; import styles from './Configure.css'; -import TagsInput from 'react-tagsinput'; +import TagsInput from 'coral-admin/src/components/TagsInput'; import t from 'coral-framework/services/i18n'; const Domainlist = ({domains, onChangeDomainlist}) => { @@ -14,8 +14,6 @@ const Domainlist = ({domains, onChangeDomainlist}) => { data.split(',').map((d) => d.trim())} onChange={(tags) => onChangeDomainlist('whitelist', tags)} /> diff --git a/client/coral-admin/src/routes/Configure/components/Wordlist.js b/client/coral-admin/src/routes/Configure/components/Wordlist.js index af8c1b383..e96f37f9f 100644 --- a/client/coral-admin/src/routes/Configure/components/Wordlist.js +++ b/client/coral-admin/src/routes/Configure/components/Wordlist.js @@ -1,6 +1,6 @@ import React from 'react'; import t from 'coral-framework/services/i18n'; -import TagsInput from 'react-tagsinput'; +import TagsInput from 'coral-admin/src/components/TagsInput'; import styles from './Configure.css'; import {Card} from 'coral-ui'; @@ -13,8 +13,6 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => ( data.split(',').map((d) => d.trim())} onChange={(tags) => onChangeWordlist('banned', tags)} /> @@ -26,8 +24,6 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => ( data.split(',').map((d) => d.trim())} onChange={(tags) => onChangeWordlist('suspect', tags)} /> diff --git a/client/coral-admin/src/routes/Install/components/Steps/PermittedDomainsStep.js b/client/coral-admin/src/routes/Install/components/Steps/PermittedDomainsStep.js index 6741954ec..20b037890 100644 --- a/client/coral-admin/src/routes/Install/components/Steps/PermittedDomainsStep.js +++ b/client/coral-admin/src/routes/Install/components/Steps/PermittedDomainsStep.js @@ -1,7 +1,7 @@ import React from 'react'; import styles from './style.css'; import {Button, Card} from 'coral-ui'; -import TagsInput from 'react-tagsinput'; +import TagsInput from 'coral-admin/src/components/TagsInput'; import t from 'coral-framework/services/i18n'; @@ -16,9 +16,6 @@ const PermittedDomainsStep = (props) => { data.split(',').map((d) => d.trim())} onChange={(tags) => handleDomainsChange(tags)} /> diff --git a/package.json b/package.json index 729358ffc..9d48f39cd 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "passport-local": "^1.0.0", "prop-types": "^15.5.10", "react-apollo": "^1.1.0", + "react-input-autosize": "^1.1.4", "react-recaptcha": "^2.2.6", "react-toastify": "^1.5.0", "react-transition-group": "^1.1.3", @@ -195,7 +196,7 @@ "react-mdl-selectfield": "^0.2.0", "react-redux": "^4.4.5", "react-router": "^3.0.0", - "react-tagsinput": "^3.14.0", + "react-tagsinput": "^3.17.0", "redux": "^3.6.0", "redux-mock-store": "^1.2.1", "redux-thunk": "^2.1.0", diff --git a/views/admin.ejs b/views/admin.ejs index 4ef4112da..54c666395 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -33,58 +33,6 @@ #root > div { height: 100%; } - - /* putting this here until I can get webpack to behave */ - .react-tagsinput { - background-color: #fff; - border: 1px solid #ccc; - overflow: hidden; - padding-left: 5px; - padding-top: 5px; - } - - .react-tagsinput--focused { - border-color: rgb(142, 76, 65); - } - - .react-tagsinput-tag { - background-color: rgb(255, 220, 214); - border-radius: 2px; - border: 1px solid rgb(244, 126, 107); - color: rgb(244, 126, 107); - display: inline-block; - font-family: sans-serif; - font-size: 13px; - font-weight: 400; - margin-bottom: 5px; - margin-right: 5px; - padding: 5px; - } - - .react-tagsinput-remove { - cursor: pointer; - font-weight: bold; - color: rgb(101, 24, 23); - } - - .react-tagsinput-tag a::before { - content: " ×"; - } - - .react-tagsinput-input { - background: transparent; - border: 0; - color: #777; - font-family: sans-serif; - font-size: 13px; - font-weight: 400; - margin-bottom: 6px; - margin-top: 1px; - outline: none; - padding: 5px; - width: 90px; - } - <% if (data != null) { %> diff --git a/yarn.lock b/yarn.lock index df7efdaf8..6e8776e08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2041,7 +2041,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2: create-hash "^1.1.0" inherits "^2.0.1" -create-react-class@^15.5.1: +create-react-class@^15.5.1, create-react-class@^15.5.2: version "15.5.2" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681" dependencies: @@ -6878,6 +6878,13 @@ react-highlight-words@^0.6.0: dependencies: highlight-words-core "^1.0.2" +react-input-autosize@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.1.4.tgz#cbc45072d4084ddc57806db8e3b34e644b8366ac" + dependencies: + create-react-class "^15.5.2" + prop-types "^15.5.8" + react-linkify@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-0.1.3.tgz#6e886180bda6c8fdc5f9f8a7ebe82fc0f48db7ad" @@ -6931,9 +6938,9 @@ react-router@^3.0.0: prop-types "^15.5.6" warning "^3.0.0" -react-tagsinput@^3.14.0: - version "3.16.1" - resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.16.1.tgz#dfb3bcbe5fc4430f60c145716c17cdc2613ce117" +react-tagsinput@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.17.0.tgz#d699bc7ff3be3492d5a3d2a57f6976908774fba3" react-toastify@^1.5.0: version "1.5.0" From fedca113d2d218c6a2bcba7bdfdd66f78ce268ec Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 26 Jun 2017 11:50:51 -0300 Subject: [PATCH 02/21] Adding plugin --- .eslintignore | 1 + .gitignore | 1 + .../src/components/Comment.js | 10 ++- .../coral-plugin-permalink/client/.babelrc | 14 ++++ .../client/.eslintrc.json | 23 ++++++ .../client/components}/PermalinkButton.js | 72 +++++++++++-------- .../client/components}/styles.css | 2 +- .../coral-plugin-permalink/client/index.js | 7 ++ plugins/coral-plugin-permalink/index.js | 1 + plugins/coral-plugin-permalink/package.json | 11 +++ 10 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 plugins/coral-plugin-permalink/client/.babelrc create mode 100644 plugins/coral-plugin-permalink/client/.eslintrc.json rename {client/coral-plugin-permalinks => plugins/coral-plugin-permalink/client/components}/PermalinkButton.js (53%) rename {client/coral-plugin-permalinks => plugins/coral-plugin-permalink/client/components}/styles.css (95%) create mode 100644 plugins/coral-plugin-permalink/client/index.js create mode 100644 plugins/coral-plugin-permalink/index.js create mode 100644 plugins/coral-plugin-permalink/package.json diff --git a/.eslintignore b/.eslintignore index 8a8c3a213..48c4f4844 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,4 +12,5 @@ plugins/* !plugins/coral-plugin-love !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content +!plugins/coral-plugin-permalink node_modules diff --git a/.gitignore b/.gitignore index b3e0eaa73..0709912e5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,6 @@ plugins/* !plugins/coral-plugin-love !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content +!plugins/coral-plugin-permalink **/node_modules/* diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 754f44558..4fa7d234b 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -1,6 +1,5 @@ import React, {PropTypes} from 'react'; -import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import AuthorName from 'coral-plugin-author-name/AuthorName'; import TagLabel from 'coral-plugin-tag-label/TagLabel'; import PubDate from 'coral-plugin-pubdate/PubDate'; @@ -509,19 +508,18 @@ export default class Comment extends React.Component { currentUserId={currentUser && currentUser.id} /> } + +
-
-
- - - { this.popover.style.top = `${this.linkButton.offsetTop - 80}px`; - this.setState({popoverOpen: !this.state.popoverOpen}); + + this.setState({ + popoverOpen: !this.state.popoverOpen + }); } handleClickOutside = () => { - this.setState({popoverOpen: false}); + this.setState({ + popoverOpen: false + }); } - copyPermalink () { + copyPermalink = () => { this.permalinkInput.select(); try { document.execCommand('copy'); - this.setState({copySuccessful: true, copyFailure: null}); + this.setState({ + copySuccessful: true, + copyFailure: null + }); } catch (err) { - this.setState({copyFailure: true, copySuccessful: null}); + this.setState({ + copyFailure: true, + copySuccessful: null + }); } setTimeout(() => { - this.setState({copyFailure: null, copySuccessful: null}); + this.setState({ + copyFailure: null, + copySuccessful: null + }); }, 3000); } render () { - const {copySuccessful, copyFailure} = this.state; + const {copySuccessful, copyFailure, popoverOpen} = this.state; + const {asset} = this.props; return (
@@ -60,15 +71,18 @@ export default class PermalinkButton extends React.Component {
this.popover = ref} - className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}> + className={cn([`${name}-popover`, styles.container, {active: popoverOpen}])}> this.permalinkInput = input} - value={`${this.props.articleURL}#${this.props.commentId}`} - onChange={() => {}} /> - -
this.popover = ref} - className={cn([`${name}-popover`, styles.container, {active: popoverOpen}])}> - this.permalinkInput = input} - value={`${asset.url}#${this.props.commentId}`} - /> - - -
-
- - ); - } -} diff --git a/plugins/coral-plugin-permalink/client/components/styles.css b/plugins/coral-plugin-permalink/client/components/styles.css deleted file mode 100644 index ba263fe0c..000000000 --- a/plugins/coral-plugin-permalink/client/components/styles.css +++ /dev/null @@ -1,67 +0,0 @@ -.container { - border-radius: 3px; - padding: 15px 10px; - box-sizing: border-box; - border: solid 1px rgba(153, 153, 153, 0.33); - left: 0; - width: 100%; - z-index: 10; - - &::before { - content: ''; - border: 10px solid transparent; - border-top-color: white; - position: absolute; - right: 7em; - bottom: -20px; - z-index: 2; - } - - &::after{ - content: ''; - border: 10px solid transparent; - border-top-color: rgba(153, 153, 153, 0.33); - position: absolute; - right: 7em; - bottom: -21px; - z-index: 1; - } - - input { - display: inline-block; - width: calc(100% - 78px); - padding: 8px; - border-radius: 3px; - border: solid 1px #e0e0e0; - height: 32px; - box-sizing: border-box; - font-size: 1em; - } - - button { - display: inline-block; - float: right; - box-sizing: border-box; - margin: 0; - background-color: #e0e0e0; - font-size: 1em; - width: auto; - height: auto; - padding: 2px; - transition: background-color 0.4s ease; - - &:hover{ - color: black; - } - - &.success { - background-color: #00897B; - color: white; - } - - &.failure { - background-color: #FF5252; - color: white; - } - } -} diff --git a/plugins/coral-plugin-permalink/client/index.js b/plugins/coral-plugin-permalink/client/index.js deleted file mode 100644 index d413da870..000000000 --- a/plugins/coral-plugin-permalink/client/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import PermalinkButton from './components/PermalinkButton'; - -export default { - slots: { - commentActions: [PermalinkButton] - } -}; diff --git a/plugins/coral-plugin-permalink/index.js b/plugins/coral-plugin-permalink/index.js deleted file mode 100644 index f053ebf79..000000000 --- a/plugins/coral-plugin-permalink/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/plugins/coral-plugin-permalink/package.json b/plugins/coral-plugin-permalink/package.json deleted file mode 100644 index 0d1f1e6ba..000000000 --- a/plugins/coral-plugin-permalink/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "coral-plugin-permalink", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC" -} From 5e1450ffb0c131e82da72e952605eda380221d21 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 26 Jun 2017 15:46:53 -0300 Subject: [PATCH 04/21] Renaming to talk --- .eslintignore | 2 +- .gitignore | 2 +- plugins/talk-plugin-permalink/client/.babelrc | 14 +++ .../client/.eslintrc.json | 23 ++++ .../client/components/PermalinkButton.js | 101 ++++++++++++++++++ .../client/components/styles.css | 66 ++++++++++++ plugins/talk-plugin-permalink/client/index.js | 7 ++ plugins/talk-plugin-permalink/index.js | 1 + plugins/talk-plugin-permalink/package.json | 11 ++ 9 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 plugins/talk-plugin-permalink/client/.babelrc create mode 100644 plugins/talk-plugin-permalink/client/.eslintrc.json create mode 100644 plugins/talk-plugin-permalink/client/components/PermalinkButton.js create mode 100644 plugins/talk-plugin-permalink/client/components/styles.css create mode 100644 plugins/talk-plugin-permalink/client/index.js create mode 100644 plugins/talk-plugin-permalink/index.js create mode 100644 plugins/talk-plugin-permalink/package.json diff --git a/.eslintignore b/.eslintignore index 48c4f4844..3a259c106 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,5 +12,5 @@ plugins/* !plugins/coral-plugin-love !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content -!plugins/coral-plugin-permalink +!plugins/talk-plugin-permalink node_modules diff --git a/.gitignore b/.gitignore index 0709912e5..0be1266c6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,6 @@ plugins/* !plugins/coral-plugin-love !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content -!plugins/coral-plugin-permalink +!plugins/talk-plugin-permalink **/node_modules/* diff --git a/plugins/talk-plugin-permalink/client/.babelrc b/plugins/talk-plugin-permalink/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-permalink/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-permalink/client/.eslintrc.json b/plugins/talk-plugin-permalink/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-permalink/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js new file mode 100644 index 000000000..b4dd190d8 --- /dev/null +++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js @@ -0,0 +1,101 @@ +import React from 'react'; +import {Button} from 'coral-ui'; +import styles from './styles.css'; +import t from 'coral-framework/services/i18n'; +import ClickOutside from 'coral-framework/components/ClickOutside'; +import cn from 'classnames'; + +const name = 'talk-plugin-permalink'; + +export default class PermalinkButton extends React.Component { + constructor (props) { + super(props); + + this.state = { + popoverOpen: false, + copySuccessful: null, + copyFailure: null + }; + + } + + toggle = () => { + this.popover.style.top = `${this.linkButton.offsetTop - 80}px`; + + this.setState({ + popoverOpen: !this.state.popoverOpen + }); + } + + handleClickOutside = () => { + this.setState({ + popoverOpen: false + }); + } + + copyPermalink = () => { + this.permalinkInput.select(); + try { + document.execCommand('copy'); + this.setState({ + copySuccessful: true, + copyFailure: null + }); + } catch (err) { + this.setState({ + copyFailure: true, + copySuccessful: null + }); + } + + setTimeout(() => { + this.setState({ + copyFailure: null, + copySuccessful: null + }); + }, 3000); + } + + render () { + const {copySuccessful, copyFailure, popoverOpen} = this.state; + const {asset} = this.props; + return ( + +
+ +
this.popover = ref} + className={cn([`${name}-popover`, styles.container, {active: popoverOpen}])}> + + this.permalinkInput = input} + value={`${asset.url}#${this.props.commentId}`} + /> + + + +
+
+
+ ); + } +} diff --git a/plugins/talk-plugin-permalink/client/components/styles.css b/plugins/talk-plugin-permalink/client/components/styles.css new file mode 100644 index 000000000..df417dd23 --- /dev/null +++ b/plugins/talk-plugin-permalink/client/components/styles.css @@ -0,0 +1,66 @@ +.container { + border-radius: 3px; + padding: 15px 10px; + box-sizing: border-box; + border: solid 1px rgba(153, 153, 153, 0.33); + left: 0; + width: 100%; + z-index: 10; +} +.container::before { + content: ''; + border: 10px solid transparent; + border-top-color: white; + position: absolute; + right: 7em; + bottom: -20px; + z-index: 2; +} + +.container::after{ + content: ''; + border: 10px solid transparent; + border-top-color: rgba(153, 153, 153, 0.33); + position: absolute; + right: 7em; + bottom: -21px; + z-index: 1; +} + +.input { + display: inline-block; + width: calc(100% - 78px); + padding: 8px; + border-radius: 3px; + border: solid 1px #e0e0e0; + height: 32px; + box-sizing: border-box; + font-size: 1em; +} + +.button { + display: inline-block; + float: right; + box-sizing: border-box; + margin: 0; + background-color: #e0e0e0; + font-size: 1em; + width: auto; + height: auto; + padding: 2px; + transition: background-color 0.4s ease; +} + +.button:hover{ + color: black; +} + +.button.success { + background-color: #00897B; + color: white; +} + +.button.failure { + background-color: #FF5252; + color: white; +} diff --git a/plugins/talk-plugin-permalink/client/index.js b/plugins/talk-plugin-permalink/client/index.js new file mode 100644 index 000000000..d413da870 --- /dev/null +++ b/plugins/talk-plugin-permalink/client/index.js @@ -0,0 +1,7 @@ +import PermalinkButton from './components/PermalinkButton'; + +export default { + slots: { + commentActions: [PermalinkButton] + } +}; diff --git a/plugins/talk-plugin-permalink/index.js b/plugins/talk-plugin-permalink/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-permalink/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/plugins/talk-plugin-permalink/package.json b/plugins/talk-plugin-permalink/package.json new file mode 100644 index 000000000..0d1f1e6ba --- /dev/null +++ b/plugins/talk-plugin-permalink/package.json @@ -0,0 +1,11 @@ +{ + "name": "coral-plugin-permalink", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} From b00d2f569eef91bce6dae967a175ef9d7d372b04 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 26 Jun 2017 16:01:07 -0300 Subject: [PATCH 05/21] Removing styles from default.css --- client/coral-embed-stream/style/default.css | 27 ------------------- .../client/components/PermalinkButton.js | 7 ++--- .../client/components/styles.css | 18 ++++++++++--- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 701a06fd5..a752493a3 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -335,33 +335,6 @@ button.comment__action-button[disabled], display: inline-block; } -.coral-plugin-permalinks-container { - /*position: relative;*/ - z-index: 2; -} - -.coral-plugin-permalinks-popover { - display: none; - background-color: white; - border: 1px solid black; - position: absolute; - padding: 5px; -} - -.coral-plugin-permalinks-popover.active { - display: block; -} - -.coral-plugin-permalinks-copy-field { - display: block; - width: calc(100% - 5px); -} - -.coral-plugin-permalinks-copied-text { - float: right; - margin: 8px; -} - /* Flag Styles */ .coral-plugin-flags-popup-form { diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js index b4dd190d8..6a92d69ce 100644 --- a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js +++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js @@ -61,7 +61,7 @@ export default class PermalinkButton extends React.Component { const {asset} = this.props; return ( -
+
+
this.popover = ref} - className={cn([`${name}-popover`, styles.container, {active: popoverOpen}])}> + className={cn([`${name}-popover`, styles.popover, {[styles.active]: popoverOpen}])}> this.permalinkInput = input} - value={`${asset.url}#${this.props.commentId}`} + defaultValue={`${asset.url}#${this.props.commentId}`} />
Date: Mon, 26 Jun 2017 16:26:05 -0300 Subject: [PATCH 08/21] renamed to talk --- plugins/talk-plugin-permalink/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/talk-plugin-permalink/package.json b/plugins/talk-plugin-permalink/package.json index 0d1f1e6ba..fefeb9f16 100644 --- a/plugins/talk-plugin-permalink/package.json +++ b/plugins/talk-plugin-permalink/package.json @@ -1,11 +1,8 @@ { - "name": "coral-plugin-permalink", + "name": "talk-plugin-permalink", "version": "1.0.0", "description": "", "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, "author": "", "license": "ISC" } From c90c177b7c9ebec6f20aad70923bd7d34dcc46a4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 27 Jun 2017 20:06:27 +0700 Subject: [PATCH 09/21] Live update on stream when banned user, suspended user or rejected username --- .../src/components/Comment.js | 14 ++- .../src/components/EditableCommentContent.js | 9 +- .../src/containers/Embed.js | 102 ++++++++++++++++++ .../src/containers/Stream.js | 4 +- client/coral-embed/src/index.js | 12 ++- client/coral-framework/reducers/auth.js | 14 +++ client/coral-plugin-commentbox/CommentBox.js | 7 ++ client/coral-plugin-flags/FlagButton.js | 2 + graph/context.js | 3 +- graph/index.js | 3 +- graph/mutators/action.js | 4 +- graph/mutators/comment.js | 28 ++--- graph/mutators/user.js | 24 +++-- graph/resolvers/subscription.js | 9 ++ graph/resolvers/user.js | 8 ++ graph/subscriptions.js | 48 ++++++++- graph/typeDefs.graphql | 23 ++++ locales/en.yml | 3 + locales/es.yml | 3 + perms/constants.js | 4 + perms/queryReducer.js | 2 + perms/subscriptionReducer.js | 6 ++ plugin-api/beta/client/hocs/withReaction.js | 4 +- .../coral-plugin-like/client/LikeButton.js | 2 + .../coral-plugin-love/client/LoveButton.js | 2 + .../client/RespectButton.js | 2 + routes/api/users/index.js | 15 ++- {graph => services}/pubsub.js | 2 +- services/users.js | 97 +++++++++-------- test/server/graph/context.js | 27 +++-- 30 files changed, 375 insertions(+), 108 deletions(-) rename {graph => services}/pubsub.js (69%) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 754f44558..86149b178 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -196,8 +196,16 @@ export default class Comment extends React.Component { editComment: React.PropTypes.func, } + editComment = () => { + return this.props.editComment(this.props.comment.id, this.props.asset.id); + } + onClickEdit (e) { e.preventDefault(); + if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { + this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + return; + } this.setState({isEditing: true}); } @@ -240,7 +248,8 @@ export default class Comment extends React.Component { } if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { this.props.setActiveReplyBox(this.props.comment.id); - return; + } else { + this.props.addNotification('error', t('error.NOT_AUTHORIZED')); } return; } @@ -469,7 +478,7 @@ export default class Comment extends React.Component { { this.state.isEditing ? { + if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { + this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + return; + } + const {editComment, addNotification, stopEditing} = this.props; if (typeof editComment !== 'function') {return;} let response; diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 68f76ccc5..2c41b0886 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -12,6 +12,8 @@ import {getDefinitionName} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from './Stream'; +import {addNotification} from 'coral-framework/actions/notification'; +import t from 'coral-framework/services/i18n'; import {setActiveTab} from '../actions/embed'; import {viewAllComments} from '../actions/stream'; @@ -20,12 +22,72 @@ const {logout, checkLogin} = authActions; const {fetchAssetSuccess} = assetActions; class EmbedContainer extends React.Component { + subscriptions = []; + + subscribeToUpdates(props = this.props) { + if (props.auth.loggedIn) { + let sub = props.data.subscribeToMore({ + document: USER_BANNED_SUBSCRIPTION, + variables: { + user_id: props.auth.user.id, + }, + updateQuery: () => { + addNotification('info', t('your_account_has_been_banned')); + }, + }); + + this.subscriptions.push(sub); + + sub = props.data.subscribeToMore({ + document: USER_SUSPENDED_SUBSCRIPTION, + variables: { + user_id: props.auth.user.id, + }, + updateQuery: () => { + addNotification('info', t('your_account_has_been_suspended')); + }, + }); + + this.subscriptions.push(sub); + + sub = props.data.subscribeToMore({ + document: USERNAME_REJECTED_SUBSCRIPTION, + variables: { + user_id: props.auth.user.id, + }, + updateQuery: () => { + addNotification('info', t('your_username_has_been_rejected')); + }, + }); + + this.subscriptions.push(sub); + } + } + + unsubscribe() { + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions = []; + } + + resubscribe(props) { + this.unsubscribe(); + this.subscribeToUpdates(props); + } + + componentDidMount() { + this.subscribeToUpdates(); + } + + componentWillUnmount() { + this.unsubscribe(); + } componentWillReceiveProps(nextProps) { if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) { // Refetch after login/logout. this.props.data.refetch(); + this.resubscribe(nextProps); } const {fetchAssetSuccess} = this.props; @@ -52,6 +114,45 @@ class EmbedContainer extends React.Component { } } +const USER_BANNED_SUBSCRIPTION = gql` + subscription UserBanned($user_id: ID!) { + userBanned(user_id: $user_id){ + id + status + canEditName + suspension { + until + } + } + } +`; + +const USER_SUSPENDED_SUBSCRIPTION = gql` + subscription UserSuspended($user_id: ID!) { + userSuspended(user_id: $user_id){ + id + status + canEditName + suspension { + until + } + } + } +`; + +const USERNAME_REJECTED_SUBSCRIPTION = gql` + subscription UsernameRejected($user_id: ID!) { + usernameRejected(user_id: $user_id){ + id + status + canEditName + suspension { + until + } + } + } +`; + const EMBED_QUERY = gql` query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) { asset(id: $assetId, url: $assetUrl) { @@ -95,6 +196,7 @@ const mapDispatchToProps = (dispatch) => setActiveTab, viewAllComments, fetchAssetSuccess, + addNotification, }, dispatch ); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index f6e1fd9ec..d66ad38d4 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -173,7 +173,7 @@ const commentFragment = gql` `; const COMMENTS_ADDED_SUBSCRIPTION = gql` - subscription onCommentAdded($assetId: ID!, $excludeIgnored: Boolean){ + subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean){ commentAdded(asset_id: $assetId){ parent { id @@ -185,7 +185,7 @@ const COMMENTS_ADDED_SUBSCRIPTION = gql` `; const COMMENTS_EDITED_SUBSCRIPTION = gql` - subscription onCommentEdited($assetId: ID!){ + subscription CommentEdited($assetId: ID!){ commentEdited(asset_id: $assetId){ id body diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 5b42fa541..82eea8ef7 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -28,6 +28,7 @@ const snackbarStyles = { // This function should return value of window.Coral const Coral = {}; const Talk = (Coral.Talk = {}); +let notificationTimeout = null; // build the URL to load in the pym iframe function buildStreamIframeUrl(talkBaseUrl, query) { @@ -110,14 +111,15 @@ function configurePymParent(pymParent, opts) { snackbar.className = `coral-notif-${type}`; snackbar.textContent = text; - setTimeout(() => { + clearTimeout(notificationTimeout); + notificationTimeout = setTimeout(() => { snackbar.style.transform = 'translate(-50%, 0)'; snackbar.style.opacity = 1; - }, 0); - setTimeout(() => { - snackbar.style.opacity = 0; - }, 5000); + notificationTimeout = setTimeout(() => { + snackbar.style.opacity = 0; + }, 7000); + }, 0); }); // Helps child show notifications at the right scrollTop diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 5a05de28e..7ab12cf53 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -151,6 +151,20 @@ export default function auth (state = initialState, action) { case actions.SET_REDIRECT_URI: return state .set('redirectUri', action.uri); + case 'APOLLO_SUBSCRIPTION_RESULT': + if (action.operationName === 'UserBanned' && state.getIn(['user', 'id']) === action.variables.user_id) { + return state + .mergeIn(['user'], action.result.data.userBanned); + } + if (action.operationName === 'UserSuspended' && state.getIn(['user', 'id']) === action.variables.user_id) { + return state + .mergeIn(['user'], action.result.data.userSuspended); + } + if (action.operationName === 'UsernameRejected' && state.getIn(['user', 'id']) === action.variables.user_id) { + return state + .mergeIn(['user'], action.result.data.usernameRejected); + } + return state; default : return state; } diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 5105667ed..c05f7bda0 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -1,6 +1,7 @@ import React, {PropTypes} from 'react'; import t from 'coral-framework/services/i18n'; +import {can} from 'coral-framework/services/perms'; import Slot from 'coral-framework/components/Slot'; import {connect} from 'react-redux'; @@ -43,8 +44,14 @@ class CommentBox extends React.Component { assetId, parentId, addNotification, + currentUser, } = this.props; + if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) { + addNotification('error', t('error.NOT_AUTHORIZED')); + return; + } + let comment = { asset_id: assetId, parent_id: parentId, diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index e8a17f201..38b1fd594 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -39,6 +39,8 @@ export default class FlagButton extends Component { } else { this.setState({showMenu: true}); } + } else { + this.props.addNotification('error', t('error.NOT_AUTHORIZED')); } } diff --git a/graph/context.js b/graph/context.js index 66086dc89..d2306cbc8 100644 --- a/graph/context.js +++ b/graph/context.js @@ -3,6 +3,7 @@ const mutators = require('./mutators'); const uuid = require('uuid'); const plugins = require('../services/plugins'); +const pubsub = require('../services/pubsub'); const debug = require('debug')('talk:graph:context'); /** @@ -32,7 +33,7 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc * Stores the request context. */ class Context { - constructor({user = null}, pubsub) { + constructor({user = null}) { // Generate a new context id for the request. this.id = uuid.v4(); diff --git a/graph/index.js b/graph/index.js index 4e99714d9..7495c0605 100644 --- a/graph/index.js +++ b/graph/index.js @@ -1,6 +1,5 @@ const schema = require('./schema'); const Context = require('./context'); -const pubsub = require('./pubsub'); const {createSubscriptionManager} = require('./subscriptions'); module.exports = { @@ -11,7 +10,7 @@ module.exports = { // Load in the new context here, this'll create the loaders + mutators for // the lifespan of this request. - context: new Context(req, pubsub) + context: new Context(req) }), createSubscriptionManager }; diff --git a/graph/mutators/action.js b/graph/mutators/action.js index 19db33183..d62d3b4c5 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -16,7 +16,7 @@ const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants'); const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, item_type, action_type, group_id, metadata = {}}) => { let comment; - if (pubsub && item_type === 'COMMENTS') { + if (item_type === 'COMMENTS') { comment = await Comments.get.load(item_id); if (!comment) { throw new Error('Comment not found'); @@ -38,7 +38,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, await UsersService.setStatus(item_id, 'PENDING'); } - if (pubsub && comment) { + if (comment) { pubsub.publish('commentFlagged', comment); } diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index df6ed71ce..f32172a4d 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -177,11 +177,8 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu } Comments.countByAssetID.incr(asset_id); - if (pubsub) { - - // Publish the newly added comment via the subscription. - pubsub.publish('commentAdded', comment); - } + // Publish the newly added comment via the subscription. + pubsub.publish('commentAdded', comment); } return comment; @@ -346,17 +343,14 @@ const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => { // adjust the affected user's karma in the next tick. process.nextTick(adjustKarma(Comments, id, status)); - if (pubsub) { + if (status === 'ACCEPTED') { - if (status === 'ACCEPTED') { + // Publish the comment status change via the subscription. + pubsub.publish('commentAccepted', comment); + } else if (status === 'REJECTED') { - // Publish the comment status change via the subscription. - pubsub.publish('commentAccepted', comment); - } else if (status === 'REJECTED') { - - // Publish the comment status change via the subscription. - pubsub.publish('commentRejected', comment); - } + // Publish the comment status change via the subscription. + pubsub.publish('commentRejected', comment); } return comment; @@ -379,11 +373,9 @@ const edit = async (context, {id, asset_id, edit: {body}}) => { // Execute the edit. const comment = await CommentsService.edit(id, context.user.id, {body, status}); - if (context.pubsub) { + // Publish the edited comment via the subscription. + context.pubsub.publish('commentEdited', comment); - // Publish the edited comment via the subscription. - context.pubsub.publish('commentEdited', comment); - } return comment; }; diff --git a/graph/mutators/user.js b/graph/mutators/user.js index cf0106a2c..448ac750a 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -2,16 +2,28 @@ const errors = require('../../errors'); const UsersService = require('../../services/users'); const {SET_USER_STATUS, SUSPEND_USER, REJECT_USERNAME} = require('../../perms/constants'); -const setUserStatus = ({user}, {id, status}) => { - return UsersService.setStatus(id, status); +const setUserStatus = async ({user, pubsub}, {id, status}) => { + const result = await UsersService.setStatus(id, status); + if (result && result.status === 'BANNED') { + pubsub.publish('userBanned', result); + } + return result; }; -const suspendUser = ({user}, {id, message, until}) => { - return UsersService.suspendUser(id, message, until); +const suspendUser = async ({user, pubsub}, {id, message, until}) => { + const result = await UsersService.suspendUser(id, message, until); + if (result) { + pubsub.publish('userSuspended', result); + } + return result; }; -const rejectUsername = ({user}, {id, message}) => { - return UsersService.rejectUsername(id, message); +const rejectUsername = async ({user, pubsub}, {id, message}) => { + const result = await UsersService.rejectUsername(id, message); + if (result) { + pubsub.publish('usernameRejected', result); + } + return result; }; const ignoreUser = ({user}, userToIgnore) => { diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js index 1f26d5ff3..f6da9fb54 100644 --- a/graph/resolvers/subscription.js +++ b/graph/resolvers/subscription.js @@ -14,6 +14,15 @@ const Subscription = { commentFlagged(comment) { return comment; }, + userBanned(user) { + return user; + }, + userSuspended(user) { + return user; + }, + usernameRejected(user) { + return user; + }, }; module.exports = Subscription; diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 035af22cb..2878d6561 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -6,6 +6,7 @@ const { SEARCH_OTHERS_COMMENTS, UPDATE_USER_ROLES, SEARCH_COMMENT_METRICS, + VIEW_SUSPENSION_INFO, LIST_OWN_TOKENS } = require('../../perms/constants'); @@ -84,6 +85,13 @@ const User = { if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) { return KarmaService.model(user); } + }, + + suspension({id, suspension}, _, {user}) { + if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) { + return null; + } + return suspension; } }; diff --git a/graph/subscriptions.js b/graph/subscriptions.js index ec2d5b786..ce1eb1a09 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -3,7 +3,7 @@ const {SubscriptionServer} = require('subscriptions-transport-ws'); const _ = require('lodash'); const debug = require('debug')('talk:graph:subscriptions'); -const pubsub = require('./pubsub'); +const pubsub = require('../services/pubsub'); const schema = require('./schema'); const Context = require('./context'); const plugins = require('../services/plugins'); @@ -21,6 +21,9 @@ const { SUBSCRIBE_COMMENT_FLAGGED, SUBSCRIBE_ALL_COMMENT_EDITED, SUBSCRIBE_ALL_COMMENT_ADDED, + SUBSCRIBE_ALL_USER_SUSPENDED, + SUBSCRIBE_ALL_USER_BANNED, + SUBSCRIBE_ALL_USERNAME_REJECTED, } = require('../perms/constants'); /** @@ -83,6 +86,45 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu } }, }), + userSuspended: (options, args) => ({ + userSuspended: { + filter: (user, context) => { + if ( + !context.user + || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + } + }, + }), + userBanned: (options, args) => ({ + userBanned: { + filter: (user, context) => { + if ( + !context.user + || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + } + }, + }), + usernameRejected: (options, args) => ({ + usernameRejected: { + filter: (user, context) => { + if ( + !context.user + || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + } + }, + }), }); /** @@ -117,10 +159,10 @@ const createSubscriptionManager = (server) => new SubscriptionServer({ } catch (e) { console.error(e); - return new Context({}, pubsub); + return new Context({}); } - return new Context(req, pubsub); + return new Context(req); }; return baseParams; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 22f4a668f..249be8d13 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -61,6 +61,10 @@ type UserProfile { provider: String! } +type SuspensionInfo { + until: Date +} + # Any person who can author comments, create actions, and view comments on a # stream. type User { @@ -108,6 +112,10 @@ type User { # returns user status status: USER_STATUS + + # returns suspension info. Only available to Admins and Moderators + # or on own logged in User. + suspension: SuspensionInfo } # UsersQuery allows the ability to query users by a specific fields. @@ -1031,6 +1039,21 @@ type Subscription { # Get an update whenever a comment has been rejected. # Requires the `ADMIN` or `MODERATOR` role. commentRejected(asset_id: ID): Comment + + # Get an update whenever a user has been suspended. + # `user_id` must match id of current user except for + # users with the `ADMIN` or `MODERATOR` role. + userSuspended(user_id: ID): User + + # Get an update whenever a user has been banned. + # `user_id` must match id of current user except for + # users with the `ADMIN` or `MODERATOR` role. + userBanned(user_id: ID): User + + # Get an update whenever a username has been rejected. + # `user_id` must match id of current user except for + # users with the `ADMIN` or `MODERATOR` role. + usernameRejected(user_id: ID): User } ################################################################################ diff --git a/locales/en.yml b/locales/en.yml index 944902e15..0051a92f2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,4 +1,7 @@ en: + your_account_has_been_suspended: Your account has been temporarily suspended. + your_account_has_been_banned: Your account has been banned. + your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username. bandialog: are_you_sure: "Are you sure you would like to ban {0}?" ban_user: "Ban User?" diff --git a/locales/es.yml b/locales/es.yml index 58bc7a132..98e2dbc0c 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -1,4 +1,7 @@ es: + your_account_has_been_suspended: Su cuenta ha sido temporalmente suspendida. + your_account_has_been_banned: Su cuenta ha sido suspendida. + your_username_has_been_rejected: Su cuenta ha sido suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. bandialog: are_you_sure: "¿Estás segura que quieres suspender a {0}?" ban_user: "¿Quieres suspender el Usuario?" diff --git a/perms/constants.js b/perms/constants.js index abfd16c6f..383f36daf 100644 --- a/perms/constants.js +++ b/perms/constants.js @@ -26,6 +26,7 @@ module.exports = { SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS', LIST_OWN_TOKENS: 'LIST_OWN_TOKENS', SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY', + VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO', // subscriptions SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED', @@ -33,4 +34,7 @@ module.exports = { SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED', SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED', SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED', + SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED', + SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED', + SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED', }; diff --git a/perms/queryReducer.js b/perms/queryReducer.js index ed84f2265..0b76c225a 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -19,6 +19,8 @@ module.exports = (user, perm) => { return check(user, ['ADMIN']); case types.SEARCH_COMMENT_STATUS_HISTORY: return check(user, ['ADMIN', 'MODERATOR']); + case types.VIEW_SUSPENSION_INFO: + return check(user, ['ADMIN', 'MODERATOR']); default: break; } diff --git a/perms/subscriptionReducer.js b/perms/subscriptionReducer.js index 0b06902cf..e0f73526a 100644 --- a/perms/subscriptionReducer.js +++ b/perms/subscriptionReducer.js @@ -13,6 +13,12 @@ module.exports = (user, perm) => { return check(user, ['ADMIN', 'MODERATOR']); case types.SUBSCRIBE_ALL_COMMENT_ADDED: return check(user, ['ADMIN', 'MODERATOR']); + case types.SUBSCRIBE_ALL_USER_SUSPENDED: + return check(user, ['ADMIN', 'MODERATOR']); + case types.SUBSCRIBE_ALL_USER_BANNED: + return check(user, ['ADMIN', 'MODERATOR']); + case types.SUBSCRIBE_ALL_USERNAME_REJECTED: + return check(user, ['ADMIN', 'MODERATOR']); default: break; } diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 1ed777996..72de7f5c4 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -8,6 +8,7 @@ import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; import withMutation from 'coral-framework/hocs/withMutation'; import {showSignInDialog} from 'coral-framework/actions/auth'; +import {addNotification} from 'coral-framework/actions/notification'; import {capitalize} from 'coral-framework/helpers/strings'; import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; import * as PropTypes from 'prop-types'; @@ -248,6 +249,7 @@ export default (reaction) => (WrappedComponent) => { return (WrappedComponent) => { }); const mapDispatchToProps = (dispatch) => - bindActionCreators({showSignInDialog}, dispatch); + bindActionCreators({showSignInDialog, addNotification}, dispatch); const enhance = compose( withFragments({ diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js index 68a516c3f..7d17ef056 100644 --- a/plugins/coral-plugin-like/client/LikeButton.js +++ b/plugins/coral-plugin-like/client/LikeButton.js @@ -13,6 +13,7 @@ class LikeButton extends React.Component { postReaction, deleteReaction, showSignInDialog, + addNotification, alreadyReacted, user, } = this.props; @@ -25,6 +26,7 @@ class LikeButton extends React.Component { // If the current user is suspended, do nothing. if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + addNotification('error', t('error.NOT_AUTHORIZED')); return; } diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index 0fc7bd7ca..7450ad668 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -13,6 +13,7 @@ class LoveButton extends React.Component { postReaction, deleteReaction, showSignInDialog, + addNotification, alreadyReacted, user, } = this.props; @@ -25,6 +26,7 @@ class LoveButton extends React.Component { // If the current user is suspended, do nothing. if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + addNotification('error', t('error.NOT_AUTHORIZED')); return; } diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js index bde8c9669..9bc556b67 100644 --- a/plugins/coral-plugin-respect/client/RespectButton.js +++ b/plugins/coral-plugin-respect/client/RespectButton.js @@ -14,6 +14,7 @@ class RespectButton extends React.Component { deleteReaction, showSignInDialog, alreadyReacted, + addNotification, user, } = this.props; @@ -25,6 +26,7 @@ class RespectButton extends React.Component { // If the current user is suspended, do nothing. if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + addNotification('error', t('error.NOT_AUTHORIZED')); return; } diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 10d72fe3a..47ff46853 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -1,8 +1,8 @@ const express = require('express'); const router = express.Router(); const UsersService = require('../../../services/users'); -const CommentsService = require('../../../services/comments'); const mailer = require('../../../services/mailer'); +const pubsub = require('../../../services/pubsub'); const errors = require('../../../errors'); const authorization = require('../../../middleware/authorization'); const i18n = require('../../../services/i18n'); @@ -53,11 +53,16 @@ router.post('/:user_id/role', authorization.needed('ADMIN'), (req, res, next) => router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) => { UsersService .setStatus(req.params.user_id, req.body.status) - .then((status) => { - res.status(201).json(status); + .then((user) => { - if (status === 'BANNED' && req.body.comment_id) { - return CommentsService.pushStatus(req.body.comment_id, 'rejected', req.params.user_id); + // TODO: current updating status behavior is weird. + if (user) { + if (user.status === 'BANNED') { + pubsub.publish('userBanned', user); + } + res.status(201).json(user.status); + } else { + res.status(500).json(); } }) .catch(next); diff --git a/graph/pubsub.js b/services/pubsub.js similarity index 69% rename from graph/pubsub.js rename to services/pubsub.js index 704f83725..a42ffb314 100644 --- a/graph/pubsub.js +++ b/services/pubsub.js @@ -1,5 +1,5 @@ const {RedisPubSub} = require('graphql-redis-subscriptions'); -const {connectionOptions} = require('../services/redis'); +const {connectionOptions} = require('./redis'); module.exports = new RedisPubSub({connection: connectionOptions}); diff --git a/services/users.js b/services/users.js index 78ec57b4c..dea208c8e 100644 --- a/services/users.js +++ b/services/users.js @@ -435,7 +435,10 @@ module.exports = class UsersService { return Promise.reject(new Error(`status ${status} is not supported`)); } - return UserModel.update({ + // TODO: current updating status behavior is weird. + // once a user has been `APPROVED` its status cannot be + // changed anymore. + return UserModel.findOneAndUpdate({ id, status: { $ne: 'APPROVED' @@ -444,6 +447,8 @@ module.exports = class UsersService { $set: { status } + }, { + new: true, }); } @@ -453,34 +458,37 @@ module.exports = class UsersService { * @param {String} message message to be send to the user * @param {Date} until date until the suspension is valid. */ - static suspendUser(id, message, until) { - return UserModel.findOneAndUpdate( + static async suspendUser(id, message, until) { + const user = await UserModel.findOneAndUpdate( {id}, { $set: { suspension: { until, }, } - }) - .then((user) => { - if (message) { - let localProfile = user.profiles.find((profile) => profile.provider === 'local'); - if (localProfile) { - const options = - { - template: 'suspension', // needed to know which template to render! - locals: { // specifies the template locals. - body: message - }, - subject: 'Your account has been suspended', - to: localProfile.id // This only works if the user has registered via e-mail. - // We may want a standard way to access a user's e-mail address in the future - }; - - return MailerService.sendSimple(options); - } - } + }, { + new: true, }); + + if (message) { + let localProfile = user.profiles.find((profile) => profile.provider === 'local'); + if (localProfile) { + const options = + { + template: 'suspension', // needed to know which template to render! + locals: { // specifies the template locals. + body: message + }, + subject: 'Your account has been suspended', + to: localProfile.id // This only works if the user has registered via e-mail. + // We may want a standard way to access a user's e-mail address in the future + }; + + await MailerService.sendSimple(options); + } + } + + return user; } /** @@ -489,34 +497,37 @@ module.exports = class UsersService { * @param {String} message message to be send to the user * @param {Date} until date until the suspension is valid. */ - static rejectUsername(id, message) { - return UserModel.findOneAndUpdate({ + static async rejectUsername(id, message) { + const user = await UserModel.findOneAndUpdate({ id }, { $set: { status: 'BANNED', canEditName: true, } - }) - .then((user) => { - if (message) { - let localProfile = user.profiles.find((profile) => profile.provider === 'local'); - if (localProfile) { - const options = - { - template: 'suspension', // needed to know which template to render! - locals: { // specifies the template locals. - body: message - }, - subject: 'Email Suspension', - to: localProfile.id // This only works if the user has registered via e-mail. - // We may want a standard way to access a user's e-mail address in the future - }; - - return MailerService.sendSimple(options); - } - } + }, { + new: true, }); + + if (message) { + let localProfile = user.profiles.find((profile) => profile.provider === 'local'); + if (localProfile) { + const options = + { + template: 'suspension', // needed to know which template to render! + locals: { // specifies the template locals. + body: message + }, + subject: 'Email Suspension', + to: localProfile.id // This only works if the user has registered via e-mail. + // We may want a standard way to access a user's e-mail address in the future + }; + + await MailerService.sendSimple(options); + } + } + + return user; } /** diff --git a/test/server/graph/context.js b/test/server/graph/context.js index b01917289..7d30042e4 100644 --- a/test/server/graph/context.js +++ b/test/server/graph/context.js @@ -3,14 +3,16 @@ const expect = require('chai').expect; const User = require('../../../models/user'); const Context = require('../../../graph/context'); const errors = require('../../../errors'); +const SettingsService = require('../../../services/settings'); describe('graph.Context', () => { + beforeEach(() => SettingsService.init()); describe('#constructor: with a user', () => { let c; beforeEach(() => { - c = new Context({user: new User({id: '1'})}); + c = new Context({user: new User({id: '1', roles: ['ADMIN']})}); }); it('creates a context with a user', (done) => { @@ -21,15 +23,10 @@ describe('graph.Context', () => { }); it('does have access to mutators', () => { - return c.mutators.Action.create({ - item_id: '1', - item_type: 'COMMENTS', - action_type: 'LIKE' - }) - .then((action) => { - expect(action).to.have.property('item_id', '1'); - expect(action).to.have.property('item_type', 'COMMENTS'); - expect(action).to.have.property('action_type', 'LIKE'); + return c.mutators.Tag.add({ + item_type: 'USERS', + id: '1', + name: 'Tag', }); }); }); @@ -48,13 +45,13 @@ describe('graph.Context', () => { }); it('does not have access to mutators', () => { - return c.mutators.Action.create({ - item_id: '1', + return c.mutators.Tag.add({ item_type: 'COMMENTS', - action_type: 'LIKE' + id: '1', + name: 'Tag', }) - .then((action) => { - expect(action).to.be.null; + .then(() => { + throw new Error('should not reach this point'); }) .catch((err) => { expect(err).to.be.equal(errors.ErrNotAuthorized); From 175dd30d274cbb35ccec451494f5396fd7d3d215 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 27 Jun 2017 21:40:48 +0700 Subject: [PATCH 10/21] Keep comment box when user is degraded (live) --- .../src/components/Stream.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index c912d1a72..457880a94 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -57,7 +57,10 @@ class Stream extends React.Component { constructor(props) { super(props); - this.state = resetCursors(this.state, props); + this.state = { + ...resetCursors(this.state, props), + keepCommentBox: false, + }; } componentWillReceiveProps(next) { @@ -68,6 +71,12 @@ class Stream extends React.Component { this.setState(resetCursors); return; } + + // Keep comment box when user was live suspended, banned, ... + if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) { + this.setState({keepCommentBox: true}); + } + if ( prevComments && nextComments && nextComments.nodes.length < prevComments.nodes.length @@ -133,6 +142,10 @@ class Stream extends React.Component { return view; } + userIsDegraged({auth: {user}} = this.props) { + return !can(user, 'INTERACT_WITH_COMMUNITY'); + } + render() { const { commentClassNames, @@ -150,6 +163,7 @@ class Stream extends React.Component { pluginProps, editName } = this.props; + const {keepCommentBox} = this.state; const view = this.getVisibleComments(); const open = asset.closedAt === null; @@ -171,6 +185,8 @@ class Stream extends React.Component { me.ignoredUsers.find((u) => u.id === comment.user.id) ); }; + + const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox); return (
@@ -199,10 +215,7 @@ class Stream extends React.Component { editName={editName} currentUsername={user.username} />} - {loggedIn && - !banned && - !temporarilySuspended && - !highlightedComment && + {showCommentBox && Date: Tue, 27 Jun 2017 21:51:19 +0700 Subject: [PATCH 11/21] Fix edit --- client/coral-embed-stream/src/components/Comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 86149b178..2178e6c44 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -196,8 +196,8 @@ export default class Comment extends React.Component { editComment: React.PropTypes.func, } - editComment = () => { - return this.props.editComment(this.props.comment.id, this.props.asset.id); + editComment = (...args) => { + return this.props.editComment(this.props.comment.id, this.props.asset.id, ...args); } onClickEdit (e) { From b67ac375ba68283da4f160931a472a2177823043 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 27 Jun 2017 22:14:06 +0700 Subject: [PATCH 12/21] Pass current user to CommentBox --- client/coral-embed-stream/src/components/Comment.js | 2 +- client/coral-embed-stream/src/components/Stream.js | 2 +- client/coral-plugin-commentbox/CommentBox.js | 6 +++--- client/coral-plugin-replies/ReplyBox.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 2178e6c44..bfa629047 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -557,8 +557,8 @@ export default class Comment extends React.Component { setActiveReplyBox={setActiveReplyBox} parentId={parentId || comment.id} addNotification={addNotification} - authorId={currentUser.id} postComment={postComment} + currentUser={currentUser} assetId={asset.id} /> : null} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 457880a94..165b3ce4c 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -224,7 +224,7 @@ class Stream extends React.Component { assetId={asset.id} premod={asset.settings.moderation} isReply={false} - authorId={user.id} + currentUser={user} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} />} diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index c05f7bda0..efa1d27ed 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -133,7 +133,7 @@ class CommentBox extends React.Component { handleChange = (e) => this.setState({body: e.target.value}); render () { - const {styles, isReply, authorId, maxCharCount} = this.props; + const {styles, isReply, currentUser, maxCharCount} = this.props; let {cancelButtonClicked} = this.props; if (isReply && typeof cancelButtonClicked !== 'function') { @@ -152,7 +152,7 @@ class CommentBox extends React.Component { charCountEnable={this.props.charCountEnable} bodyPlaceholder={t('comment.comment')} bodyInputId={isReply ? 'replyText' : 'commentText'} - saveComment={authorId && this.postComment} + saveComment={currentUser && this.postComment} buttonContainerStart={ From 97ab338486a219e9f40c7e2d2f47ae3749894ae0 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 27 Jun 2017 23:51:49 +0700 Subject: [PATCH 13/21] Don't alias username --- client/coral-embed-stream/src/containers/Comment.js | 2 +- client/coral-plugin-author-name/AuthorName.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index f5682bdd1..040d1a83e 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -34,7 +34,7 @@ export default withFragments({ } user { id - name: username + username } action_summaries { __typename diff --git a/client/coral-plugin-author-name/AuthorName.js b/client/coral-plugin-author-name/AuthorName.js index 66b907ad8..7d453d96f 100644 --- a/client/coral-plugin-author-name/AuthorName.js +++ b/client/coral-plugin-author-name/AuthorName.js @@ -24,7 +24,7 @@ export default class AuthorName extends Component { return (
- {author && author.name} + {author && author.username}
); } From b134d4099cf5ea892c000fb22e913dc98a4ea534 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 28 Jun 2017 00:13:47 +0700 Subject: [PATCH 14/21] More concise and functional subscribing --- .../src/containers/Embed.js | 35 +++++++------------ .../src/containers/Stream.js | 23 ++++++------ 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 2c41b0886..bf05caf29 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -26,41 +26,32 @@ class EmbedContainer extends React.Component { subscribeToUpdates(props = this.props) { if (props.auth.loggedIn) { - let sub = props.data.subscribeToMore({ + const newSubscriptions = [{ document: USER_BANNED_SUBSCRIPTION, - variables: { - user_id: props.auth.user.id, - }, updateQuery: () => { addNotification('info', t('your_account_has_been_banned')); }, - }); - - this.subscriptions.push(sub); - - sub = props.data.subscribeToMore({ + }, + { document: USER_SUSPENDED_SUBSCRIPTION, - variables: { - user_id: props.auth.user.id, - }, updateQuery: () => { addNotification('info', t('your_account_has_been_suspended')); }, - }); - - this.subscriptions.push(sub); - - sub = props.data.subscribeToMore({ + }, + { document: USERNAME_REJECTED_SUBSCRIPTION, - variables: { - user_id: props.auth.user.id, - }, updateQuery: () => { addNotification('info', t('your_username_has_been_rejected')); }, - }); + }]; - this.subscriptions.push(sub); + this.subscriptions = newSubscriptions.map((s) => props.data.subscribeToMore({ + document: s.document, + variables: { + user_id: props.auth.user.id, + }, + updateQuery: s.updateQuery, + })); } } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index d66ad38d4..3491fc846 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -30,11 +30,8 @@ class StreamContainer extends React.Component { subscriptions = []; subscribeToUpdates() { - const sub1 = this.props.data.subscribeToMore({ + const newSubscriptions = [{ document: COMMENTS_EDITED_SUBSCRIPTION, - variables: { - assetId: this.props.root.asset.id, - }, updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => { // Ignore mutations from me. @@ -52,13 +49,9 @@ class StreamContainer extends React.Component { return removeCommentFromEmbedQuery(prev, commentEdited.id); } }, - }); - - const sub2 = this.props.data.subscribeToMore({ + }, + { document: COMMENTS_ADDED_SUBSCRIPTION, - variables: { - assetId: this.props.root.asset.id, - }, updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => { // Ignore mutations from me. @@ -81,9 +74,15 @@ class StreamContainer extends React.Component { return insertCommentIntoEmbedQuery(prev, commentAdded); } - }); + }]; - this.subscriptions.push(sub1, sub2); + this.subscriptions = newSubscriptions.map((s) => this.props.data.subscribeToMore({ + document: s.document, + variables: { + assetId: this.props.root.asset.id, + }, + updateQuery: s.updateQuery, + })); } unsubscribe() { From 35ccc5070d6b2235a539c75ba2e6e4ebc6ae8599 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 27 Jun 2017 14:52:12 -0300 Subject: [PATCH 15/21] Flag refactor --- .../src/components/Comment.js | 2 +- .../{ => components}/FlagButton.js | 0 .../{ => components}/FlagComment.js | 19 ++++++++++--------- client/coral-plugin-flags/helpers/flagMap.js | 15 +++++++++++++++ client/coral-plugin-flags/index.js | 1 + 5 files changed, 27 insertions(+), 10 deletions(-) rename client/coral-plugin-flags/{ => components}/FlagButton.js (100%) rename client/coral-plugin-flags/{ => components}/FlagComment.js (56%) create mode 100644 client/coral-plugin-flags/helpers/flagMap.js create mode 100644 client/coral-plugin-flags/index.js diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 754f44558..efa88d0dd 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -5,7 +5,7 @@ import AuthorName from 'coral-plugin-author-name/AuthorName'; import TagLabel from 'coral-plugin-tag-label/TagLabel'; import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; -import FlagComment from 'coral-plugin-flags/FlagComment'; +import {FlagComment} from 'coral-plugin-flags'; import {can} from 'coral-framework/services/perms'; import {TransitionGroup} from 'react-transition-group'; import cn from 'classnames'; diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/components/FlagButton.js similarity index 100% rename from client/coral-plugin-flags/FlagButton.js rename to client/coral-plugin-flags/components/FlagButton.js diff --git a/client/coral-plugin-flags/FlagComment.js b/client/coral-plugin-flags/components/FlagComment.js similarity index 56% rename from client/coral-plugin-flags/FlagComment.js rename to client/coral-plugin-flags/components/FlagComment.js index efc4c282f..ddad63b7e 100644 --- a/client/coral-plugin-flags/FlagComment.js +++ b/client/coral-plugin-flags/components/FlagComment.js @@ -2,6 +2,7 @@ import React from 'react'; import FlagButton from './FlagButton'; import t from 'coral-framework/services/i18n'; +import * as flagReason from '../helpers/flagMap'; const FlagComment = (props) => ; @@ -20,17 +21,17 @@ const getPopupMenu = [ (itemType) => { const options = itemType === 'COMMENTS' ? [ - {val: 'This comment is offensive', text: t('comment_offensive')}, - {val: 'This looks like an ad/marketing', text: t('marketing')}, - {val: 'I don\'t agree with this comment', text: t('no_agree_comment')}, - {val: 'Other', text: t('other')} + {val: flagReason.commment.offensive, text: t('comment_offensive')}, + {val: flagReason.commment.spam, text: t('marketing')}, + {val: flagReason.commment.noagree, text: t('no_agree_comment')}, + {val: flagReason.commment.other, text: t('other')} ] : [ - {val: 'This username is offensive', text: t('username_offensive')}, - {val: 'I don\'t like this username', text: t('no_like_username')}, - {val: 'This user is impersonating', text: t('user_impersonating')}, - {val: 'This looks like an ad/marketing', text: t('marketing')}, - {val: 'Other', text: t('other')} + {val: flagReason.username.offensive, text: t('username_offensive')}, + {val: flagReason.username.nolike, text: t('no_like_username')}, + {val: flagReason.username.impersonating, text: t('user_impersonating')}, + {val: flagReason.username.spam, text: t('marketing')}, + {val: flagReason.username.other, text: t('other')} ]; return { header: t('step_2_header'), diff --git a/client/coral-plugin-flags/helpers/flagMap.js b/client/coral-plugin-flags/helpers/flagMap.js new file mode 100644 index 000000000..514c57398 --- /dev/null +++ b/client/coral-plugin-flags/helpers/flagMap.js @@ -0,0 +1,15 @@ +export default { + username: { + offensive: 'USERNAME_OFFENSIVE', + nolike: 'USERNAME_NOLIKE', + impersonating: 'USERNAME_IMPERSONATING', + spam: 'USERNAME_SPAM', + other: 'USERNAME_OTHER' + }, + comment: { + offensive: 'COMMENT_OFFENSIVE', + spam: 'COMMENT_SPAM', + noagree: 'COMMENT_NOAGREE', + other: 'COMMENT_OTHER' + } +}; diff --git a/client/coral-plugin-flags/index.js b/client/coral-plugin-flags/index.js new file mode 100644 index 000000000..d896d34f5 --- /dev/null +++ b/client/coral-plugin-flags/index.js @@ -0,0 +1 @@ +export {default as FlagComment} from './components/FlagComment'; \ No newline at end of file From f13682c12585ea98f9798d629ab4de2e0fc7695d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 27 Jun 2017 16:12:13 -0300 Subject: [PATCH 16/21] flagMap added to the admin side --- .../src/routes/Community/components/User.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/routes/Community/components/User.js b/client/coral-admin/src/routes/Community/components/User.js index ee917e295..85f8cbd27 100644 --- a/client/coral-admin/src/routes/Community/components/User.js +++ b/client/coral-admin/src/routes/Community/components/User.js @@ -2,17 +2,18 @@ import React from 'react'; import styles from './Community.css'; import ActionButton from './ActionButton'; +import {username} from 'coral-plugin-flags/helpers/flagMap'; 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 = { - 'This comment is offensive': t('community.offensive'), - 'This looks like an ad/marketing': t('community.spam_ads'), - 'This user is impersonating': t('community.impersonating'), - 'I don\'t like this username': t('community.dont_like_username'), - 'Other': t('community.other') + [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 From e03e15fb3bf7d58690bdeec71d23ef92a362fe99 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 27 Jun 2017 16:20:08 -0300 Subject: [PATCH 17/21] renaming --- .../src/routes/Community/components/User.js | 2 +- .../components/FlagComment.js | 20 +++++++++---------- .../helpers/{flagMap.js => flagReasons.js} | 0 3 files changed, 11 insertions(+), 11 deletions(-) rename client/coral-plugin-flags/helpers/{flagMap.js => flagReasons.js} (100%) diff --git a/client/coral-admin/src/routes/Community/components/User.js b/client/coral-admin/src/routes/Community/components/User.js index 85f8cbd27..b6a01f016 100644 --- a/client/coral-admin/src/routes/Community/components/User.js +++ b/client/coral-admin/src/routes/Community/components/User.js @@ -2,7 +2,7 @@ import React from 'react'; import styles from './Community.css'; import ActionButton from './ActionButton'; -import {username} from 'coral-plugin-flags/helpers/flagMap'; +import {username} from 'coral-plugin-flags/helpers/flagReasons'; import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; diff --git a/client/coral-plugin-flags/components/FlagComment.js b/client/coral-plugin-flags/components/FlagComment.js index ddad63b7e..fc67d4b71 100644 --- a/client/coral-plugin-flags/components/FlagComment.js +++ b/client/coral-plugin-flags/components/FlagComment.js @@ -2,7 +2,7 @@ import React from 'react'; import FlagButton from './FlagButton'; import t from 'coral-framework/services/i18n'; -import * as flagReason from '../helpers/flagMap'; +import {username, comment} from '../helpers/flagReasons'; const FlagComment = (props) => ; @@ -21,17 +21,17 @@ const getPopupMenu = [ (itemType) => { const options = itemType === 'COMMENTS' ? [ - {val: flagReason.commment.offensive, text: t('comment_offensive')}, - {val: flagReason.commment.spam, text: t('marketing')}, - {val: flagReason.commment.noagree, text: t('no_agree_comment')}, - {val: flagReason.commment.other, text: t('other')} + {val: comment.offensive, text: t('comment_offensive')}, + {val: comment.spam, text: t('marketing')}, + {val: comment.noagree, text: t('no_agree_comment')}, + {val: comment.other, text: t('other')} ] : [ - {val: flagReason.username.offensive, text: t('username_offensive')}, - {val: flagReason.username.nolike, text: t('no_like_username')}, - {val: flagReason.username.impersonating, text: t('user_impersonating')}, - {val: flagReason.username.spam, text: t('marketing')}, - {val: flagReason.username.other, text: t('other')} + {val: username.offensive, text: t('username_offensive')}, + {val: username.nolike, text: t('no_like_username')}, + {val: username.impersonating, text: t('user_impersonating')}, + {val: username.spam, text: t('marketing')}, + {val: username.other, text: t('other')} ]; return { header: t('step_2_header'), diff --git a/client/coral-plugin-flags/helpers/flagMap.js b/client/coral-plugin-flags/helpers/flagReasons.js similarity index 100% rename from client/coral-plugin-flags/helpers/flagMap.js rename to client/coral-plugin-flags/helpers/flagReasons.js From dd76ac1ed4fb5fbdd27c473eccf27d7637f06dbc Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 27 Jun 2017 16:51:00 -0300 Subject: [PATCH 18/21] Now the smooth flag works --- client/coral-admin/src/index.js | 2 + package.json | 1 + .../client/components/CopyButton.js | 16 +++ .../client/components/Permalink.js | 107 ++++++++++++++++++ yarn.lock | 4 + 5 files changed, 130 insertions(+) create mode 100644 plugins/talk-plugin-permalink/client/components/CopyButton.js create mode 100644 plugins/talk-plugin-permalink/client/components/Permalink.js diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 2f2e75275..29df3753c 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,6 +1,7 @@ import React from 'react'; import {render} from 'react-dom'; import {ApolloProvider} from 'react-apollo'; +import smoothscroll from 'smoothscroll-polyfill'; import {getClient} from './services/client'; import store from './services/store'; @@ -13,6 +14,7 @@ import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/he loadPluginsTranslations(); injectPluginsReducers(); +smoothscroll.polyfill(); render( diff --git a/package.json b/package.json index 9d48f39cd..2834a3e3a 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "resolve": "^1.3.2", "semver": "^5.3.0", "simplemde": "^1.11.2", + "smoothscroll-polyfill": "^0.3.5", "snake-case": "^2.1.0", "subscriptions-transport-ws": "^0.7.2", "timekeeper": "^1.0.0", diff --git a/plugins/talk-plugin-permalink/client/components/CopyButton.js b/plugins/talk-plugin-permalink/client/components/CopyButton.js new file mode 100644 index 000000000..405468a6e --- /dev/null +++ b/plugins/talk-plugin-permalink/client/components/CopyButton.js @@ -0,0 +1,16 @@ +import React from 'react'; +import {t} from 'plugin-api/beta/client/services'; +import {withCopyToClipboard} from 'plugin-api/beta/client/hocs'; +import {Button} from 'plugin-api/beta/client/components/ui'; + +class ButtonCopyToClipboard extends React.Component { + render () { + return ( + + ); + } +} + +export default withCopyToClipboard(ButtonCopyToClipboard); diff --git a/plugins/talk-plugin-permalink/client/components/Permalink.js b/plugins/talk-plugin-permalink/client/components/Permalink.js new file mode 100644 index 000000000..68406472e --- /dev/null +++ b/plugins/talk-plugin-permalink/client/components/Permalink.js @@ -0,0 +1,107 @@ +import React from 'react'; +import cn from 'classnames'; +import styles from './styles.css'; +import {t} from 'plugin-api/beta/client/services'; +import {ClickOutside} from 'plugin-api/beta/client/components'; +import {Icon, Button} from 'plugin-api/beta/client/components/ui'; + +const name = 'talk-plugin-permalink'; + +export default class PermalinkButton extends React.Component { + constructor (props) { + super(props); + + this.state = { + popoverOpen: false, + copySuccessful: null, + copyFailure: null + }; + + } + + toggle = () => { + this.popover.style.top = `${this.linkButton.offsetTop - 80}px`; + + this.setState({ + popoverOpen: !this.state.popoverOpen + }); + } + + handleClickOutside = () => { + this.setState({ + popoverOpen: false + }); + } + + copyPermalink = () => { + this.permalinkInput.select(); + try { + document.execCommand('copy'); + this.setState({ + copySuccessful: true, + copyFailure: null + }); + } catch (err) { + this.setState({ + copyFailure: true, + copySuccessful: null + }); + } + + this.timeout = window.setTimeout(() => { + this.setState({ + copyFailure: null, + copySuccessful: null + }); + }, 3000); + } + + componentWillUnmount() { + window.clearTimeout(this.timeout); + } + + render () { + const {copySuccessful, copyFailure, popoverOpen} = this.state; + const {asset} = this.props; + return ( + +
+ + + +
this.popover = ref} + className={cn([`${name}-popover`, styles.popover, {[styles.active]: popoverOpen}])}> + + this.permalinkInput = input} + defaultValue={`${asset.url}#${this.props.commentId}`} + /> + + + +
+
+
+ ); + } +} diff --git a/yarn.lock b/yarn.lock index 6e8776e08..92edf11b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7602,6 +7602,10 @@ smart-buffer@^1.0.4: version "1.1.15" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" +smoothscroll-polyfill@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.3.5.tgz#466e6039b51cb525d70e1a5077ef81e064678eae" + smtp-connection@2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/smtp-connection/-/smtp-connection-2.12.0.tgz#d76ef9127cb23c2259edb1e8349c2e8d5e2d74c1" From 270491dc0f346cb48a47bc8ba012e868f1c11a93 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 28 Jun 2017 16:08:46 +0700 Subject: [PATCH 19/21] Allow editing to the same username in the "create username" flow --- services/users.js | 19 +++++++++++++++--- test/server/routes/api/account/index.js | 17 ---------------- test/server/services/users.js | 26 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/services/users.js b/services/users.js index 78ec57b4c..011e8e3ca 100644 --- a/services/users.js +++ b/services/users.js @@ -863,11 +863,24 @@ module.exports = class UsersService { * @return {Promise} */ static async editName(id, username) { + + // TODO: Revisit this when we revamped User status workflows. + const queryUsernameRejected = { + id, + username: {$ne: username}, + status: 'BANNED', + canEditName: true + }; + + const queryCreateUsername = { + id, + status: 'ACTIVE', + canEditName: true + }; + try { const result = await UserModel.findOneAndUpdate({ - id, - username: {$ne: username}, - canEditName: true + $or: [queryUsernameRejected, queryCreateUsername], }, { $set: { username: username, diff --git a/test/server/routes/api/account/index.js b/test/server/routes/api/account/index.js index 08b9c8b1d..8d4e0057a 100644 --- a/test/server/routes/api/account/index.js +++ b/test/server/routes/api/account/index.js @@ -54,23 +54,6 @@ describe('/api/v1/account/username', () => { }); }); - it('it should return an error when the user submits the same username', (done) => { - chai.request(app) - .post(`/api/v1/users/${mockUser.id}/username-enable`) - .set(passport.inject({id: '456', roles: ['ADMIN']})) - .then(() => chai.request(app) - .put('/api/v1/account/username') - .set(passport.inject({id: mockUser.id, roles: []})) - .send({username: 'Ana'})) - .then(() => { - done(new Error('Expected Error')); - }) - .catch((err) => { - expect(err).to.be.ok; - done(); - }); - }); - it('it should return an error when the user tries to edit their username if canEditName is disabled', (done) => { chai.request(app) .put('/api/v1/account/username') diff --git a/test/server/services/users.js b/test/server/services/users.js index 027ac524c..53f4bc018 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -238,6 +238,32 @@ describe('services.UsersService', () => { }); }); + it('should let the user submit the same username if user is not banned (create username)', () => { + return UsersService + .toggleNameEdit(mockUsers[0].id, true) + .then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username)) + .then(() => UsersService.findById(mockUsers[0].id)) + .then((user) => { + expect(user).to.have.property('username', mockUsers[0].username); + expect(user).to.have.property('canEditName', false); + }); + }); + + it('should return error when a banned user submits the same username (rejected username)', () => { + return UsersService + .toggleNameEdit(mockUsers[0].id, true) + .then(() => UsersService.setStatus(mockUsers[0].id, 'BANNED')) + .then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username)) + .then(() => UsersService.findById(mockUsers[0].id)) + .then(() => { + throw new Error('Error expected'); + }) + .catch((err) => { + expect(err.status).to.equal(400); + expect(err.translation_key).to.equal('SAME_USERNAME_PROVIDED'); + }); + }); + it('should return an error if canEditName is false', (done) => { UsersService .editName(mockUsers[0].id, 'Jojo') From f767b6a99806b5e19fd326ffbef57c7ffa134533 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 28 Jun 2017 09:06:46 -0300 Subject: [PATCH 20/21] Removing old --- .../client/components/CopyButton.js | 16 --- .../client/components/Permalink.js | 107 ------------------ 2 files changed, 123 deletions(-) delete mode 100644 plugins/talk-plugin-permalink/client/components/CopyButton.js delete mode 100644 plugins/talk-plugin-permalink/client/components/Permalink.js diff --git a/plugins/talk-plugin-permalink/client/components/CopyButton.js b/plugins/talk-plugin-permalink/client/components/CopyButton.js deleted file mode 100644 index 405468a6e..000000000 --- a/plugins/talk-plugin-permalink/client/components/CopyButton.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import {t} from 'plugin-api/beta/client/services'; -import {withCopyToClipboard} from 'plugin-api/beta/client/hocs'; -import {Button} from 'plugin-api/beta/client/components/ui'; - -class ButtonCopyToClipboard extends React.Component { - render () { - return ( - - ); - } -} - -export default withCopyToClipboard(ButtonCopyToClipboard); diff --git a/plugins/talk-plugin-permalink/client/components/Permalink.js b/plugins/talk-plugin-permalink/client/components/Permalink.js deleted file mode 100644 index 68406472e..000000000 --- a/plugins/talk-plugin-permalink/client/components/Permalink.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import styles from './styles.css'; -import {t} from 'plugin-api/beta/client/services'; -import {ClickOutside} from 'plugin-api/beta/client/components'; -import {Icon, Button} from 'plugin-api/beta/client/components/ui'; - -const name = 'talk-plugin-permalink'; - -export default class PermalinkButton extends React.Component { - constructor (props) { - super(props); - - this.state = { - popoverOpen: false, - copySuccessful: null, - copyFailure: null - }; - - } - - toggle = () => { - this.popover.style.top = `${this.linkButton.offsetTop - 80}px`; - - this.setState({ - popoverOpen: !this.state.popoverOpen - }); - } - - handleClickOutside = () => { - this.setState({ - popoverOpen: false - }); - } - - copyPermalink = () => { - this.permalinkInput.select(); - try { - document.execCommand('copy'); - this.setState({ - copySuccessful: true, - copyFailure: null - }); - } catch (err) { - this.setState({ - copyFailure: true, - copySuccessful: null - }); - } - - this.timeout = window.setTimeout(() => { - this.setState({ - copyFailure: null, - copySuccessful: null - }); - }, 3000); - } - - componentWillUnmount() { - window.clearTimeout(this.timeout); - } - - render () { - const {copySuccessful, copyFailure, popoverOpen} = this.state; - const {asset} = this.props; - return ( - -
- - - -
this.popover = ref} - className={cn([`${name}-popover`, styles.popover, {[styles.active]: popoverOpen}])}> - - this.permalinkInput = input} - defaultValue={`${asset.url}#${this.props.commentId}`} - /> - - - -
-
-
- ); - } -} From b92afff4e7fb6eb632e0000b436392e4d992ee1e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 28 Jun 2017 21:14:34 +0700 Subject: [PATCH 21/21] Don't alias username 2 --- client/coral-embed-stream/src/components/Comment.js | 2 +- client/coral-embed-stream/src/components/IgnoreUserWizard.js | 4 ++-- client/coral-embed-stream/src/components/TopRightMenu.js | 2 +- client/coral-plugin-replies/ReplyBox.js | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6e3c645be..7a8b13312 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -169,7 +169,7 @@ export default class Comment extends React.Component { replies: PropTypes.object, user: PropTypes.shape({ id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired + username: PropTypes.string.isRequired }).isRequired, editing: PropTypes.shape({ edited: PropTypes.bool, diff --git a/client/coral-embed-stream/src/components/IgnoreUserWizard.js b/client/coral-embed-stream/src/components/IgnoreUserWizard.js index 7e72bb924..b7612b9f8 100644 --- a/client/coral-embed-stream/src/components/IgnoreUserWizard.js +++ b/client/coral-embed-stream/src/components/IgnoreUserWizard.js @@ -9,7 +9,7 @@ export class IgnoreUserWizard extends React.Component { // comment on which this menu appears user: PropTypes.shape({ id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired + username: PropTypes.string.isRequired }).isRequired, cancel: PropTypes.func.isRequired, @@ -47,7 +47,7 @@ export class IgnoreUserWizard extends React.Component { const step2Confirmation = (
Ignore User
-

Are you sure you want to ignore { user.name }?

+

Are you sure you want to ignore { user.username }?

diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js index d65722b6d..4cd544379 100644 --- a/client/coral-embed-stream/src/components/TopRightMenu.js +++ b/client/coral-embed-stream/src/components/TopRightMenu.js @@ -12,7 +12,7 @@ export class TopRightMenu extends React.Component { comment: PropTypes.shape({ user: PropTypes.shape({ id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired + username: PropTypes.string.isRequired }).isRequired }).isRequired, ignoreUser: PropTypes.func, diff --git a/client/coral-plugin-replies/ReplyBox.js b/client/coral-plugin-replies/ReplyBox.js index b8b983fe4..cc8e8045f 100644 --- a/client/coral-plugin-replies/ReplyBox.js +++ b/client/coral-plugin-replies/ReplyBox.js @@ -48,7 +48,6 @@ ReplyBox.propTypes = { commentPostedHandler: PropTypes.func, parentId: PropTypes.string, addNotification: PropTypes.func.isRequired, - authorId: PropTypes.string.isRequired, postComment: PropTypes.func.isRequired, assetId: PropTypes.string.isRequired };