diff --git a/README.md b/README.md index 0f217d89c..0b77e95b7 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,14 @@ A commenting platform from The Coral Project. [https://coralproject.net](https:/ ## Contributing to Talk +### Product Roadmap +You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625 + +You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk + ### Local Dependencies Node + Mongo ### Getting Started @@ -19,13 +25,19 @@ Runs Talk. The Talk application requires specific configuration options to be available inside the environment in order to run, those variables are listed here: -- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to - secure cookies +- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to +secure cookies. - `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook - Login enabled app. +Login enabled app. - `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your - Facebook Login enabled app. -- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `://` without the path. +Facebook Login enabled app. +- `TALK_ROOT_URL` (*required*) - root url of the installed application externally +available in the format: `://` without the path. +- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name. +- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using. +- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using. +- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`. +- `TALK_SMTP_PORT` (*required*) - SMTP port. ### Running with Docker Make sure you have Docker running first and then run `docker-compose up -d` @@ -37,9 +49,11 @@ Make sure you have Docker running first and then run `docker-compose up -d` `npm run lint` ### Helpful URLs -Bare comment stream: http://localhost:5000/client/coral-embed-stream/ -Comment stream embedded on sample article: http://localhost:5000/client/coral-embed-stream/samplearticle.html -Moderator view: http://localhost:5000/admin/ +Comment stream: http://localhost:3000/ + +Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html + +Moderator view: http://localhost:3000/admin ### Docs `swagger.yaml` diff --git a/app.js b/app.js index dd58e9883..fe392932b 100644 --- a/app.js +++ b/app.js @@ -94,7 +94,9 @@ app.use((req, res, next) => { // returning a status code that makes sense. app.use('/api', (err, req, res, next) => { if (err !== ErrNotFound) { - console.error(err); + if (app.get('env') !== 'test') { + console.error(err); + } } res.status(err.status || 500); diff --git a/bin/cli-settings b/bin/cli-settings index c639a5ca5..e7ba30151 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -15,7 +15,7 @@ const mongoose = require('../mongoose'); const Setting = require('../models/setting'); const util = require('../util'); -// Regeister the shutdown criteria. +// Register the shutdown criteria. util.onshutdown([ () => mongoose.disconnect() ]); diff --git a/bin/cli-users b/bin/cli-users index 2e0dc84e6..adfe3bf23 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -34,6 +34,7 @@ function createUser(options) { email: options.email, password: options.password, displayName: options.name, + role: options.role }); } @@ -62,6 +63,11 @@ function createUser(options) { name: 'displayName', description: 'Display Name', required: true + }, + { + name: 'role', + description: 'User Role', + required: false } ], (err, result) => { if (err) { @@ -76,15 +82,21 @@ function createUser(options) { }); }) .then((result) => { - return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim()); - }) - .then((user) => { - console.log(`Created user ${user.id}.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(); + return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim()) + .then((user) => { + console.log(`Created user ${user.id}.`); + + return User + .addRoleToUser(user.id, result.role.trim()) + .then(() => { + console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`); + util.shutdown(); + }); + }) + .catch((err) => { + console.error(err); + util.shutdown(); + }); }); } @@ -207,6 +219,7 @@ function listUsers() { 'Display Name', 'Profiles', 'Roles', + 'Status', 'State' ] }); @@ -217,6 +230,7 @@ function listUsers() { user.displayName, user.profiles.map((p) => p.provider).join(', '), user.roles.join(', '), + user.status, user.disabled ? 'Disabled' : 'Enabled' ]); }); @@ -284,6 +298,40 @@ function removeRole(userID, role) { }); } +/** + * Ban a user + * @param {String} userID id of the user to ban + */ +function ban(userID) { + User + .setStatus(userID, 'banned', '') + .then(() => { + console.log(`Banned the User ${userID}.`); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); +} + +/** + * Unban a user + * @param {String} userUD id of the user to remove the role from + */ +function unban(userID) { + User + .setStatus(userID, 'active', '') + .then(() => { + console.log(`Unban the User ${userID}.`); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); +} + /** * Disable a given user. * @param {String} userID the ID of a user to disable @@ -330,6 +378,7 @@ program .option('--email [email]', 'Email to use') .option('--password [password]', 'Password to use') .option('--name [name]', 'Name to use') + .option('--role [role]', 'Role to add') .option('-f, --flag_mode', 'Source from flags instead of prompting') .description('create a new user') .action(createUser); @@ -371,6 +420,16 @@ program .description('removes a role from a given user') .action(removeRole); +program + .command('ban ') + .description('ban a given user') + .action(ban); + +program + .command('uban ') + .description('unban a given user') + .action(unban); + program .command('disable ') .description('disable a given user from logging in') diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index e4a55a893..d4aee034e 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -1,4 +1,3 @@ - /** * Action disptacher related to comments */ @@ -16,3 +15,16 @@ export const flagComment = id => (dispatch, getState) => { export const createComment = (name, body) => dispatch => { dispatch({type: 'COMMENT_CREATE', name, body}); }; + +// Dialog Actions +export const showBanUserDialog = (userId, userName, commentId) => { + return dispatch => { + dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId}); + }; +}; + +export const hideBanUserDialog = (showDialog) => { + return dispatch => { + dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog}); + }; +}; diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 8b8e883d8..24c1b403d 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -6,7 +6,8 @@ import { FETCH_COMMENTERS_FAILURE, SORT_UPDATE, COMMENTERS_NEW_PAGE, - SET_ROLE + SET_ROLE, + SET_COMMENTER_STATUS } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; @@ -46,3 +47,10 @@ export const setRole = (id, role) => dispatch => { return dispatch({type: SET_ROLE, id, role}); }); }; + +export const setCommenterStatus = (id, status) => dispatch => { + return coralApi(`/user/${id}/status`, {method: 'POST', body: {status}}) + .then(() => { + return dispatch({type: SET_COMMENTER_STATUS, id, status}); + }); +}; diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js new file mode 100644 index 000000000..f2ff37cbd --- /dev/null +++ b/client/coral-admin/src/actions/users.js @@ -0,0 +1,14 @@ + +/** + * Action disptacher related to users + */ +// +// export const banUser = (status, author_id) => (dispatch) => { +// dispatch({type: 'USER_STATUS_UPDATE', author_id, status}); +// }; +export const banUser = (status, userId, commentId) => { + return dispatch => { + dispatch({type: 'USER_BAN', status, userId, commentId}); + dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'}); + }; +}; diff --git a/client/coral-admin/src/components/BanUserDialog.css b/client/coral-admin/src/components/BanUserDialog.css new file mode 100644 index 000000000..dfac4f194 --- /dev/null +++ b/client/coral-admin/src/components/BanUserDialog.css @@ -0,0 +1,147 @@ +.dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 280px; + top: 10px; +} + +.header { + margin-bottom: 20px; +} + +.header h1, .separator h1{ + text-align: center; + font-size: 1.2em; +} + +.formField { + margin-top: 15px; +} + +.formField label { + font-size: 1.08em; + font-weight: bold; + margin-bottom: 5px; +} + +.formField input { + width: 100%; + display: block; + border: none; + outline: none; + border: 1px solid rgba(0,0,0,.12); + padding: 10px 6px; + box-sizing: border-box; + border-radius: 2px; + margin: 5px auto; +} + +.footer { + margin: 20px auto 10px; + text-align: center; +} + +.footer span { + display: block; + margin-bottom: 5px; +} + +.footer a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; +} + +.socialConnections { + margin-bottom: 20px; +} + +.signInButton { + margin-top: 10px; +} + +.close { + font-size: 20px; + line-height: 14px; + top: 10px; + right: 10px; + position: absolute; + display: block; + font-weight: bold; + color: #363636; + cursor: pointer; +} + +.close:hover { + color: #6b6b6b; +} + +input.error{ + border: solid 2px #f44336; +} + +.errorMsg, .hint { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} + +.alert { + padding: 10px; + margin-bottom: 20px; + border-radius: 2px; +} + +.alert--success { + border: solid 1px #1ec00e; + background: #cbf1b8; + color: #006900; +} + +.alert--error { + background: #FFEBEE; + color: #B71C1C; +} + +.userBox a { + color: #2c69b6; + cursor: pointer; + margin: 0px; +} + +.attention { + display: inline-block; + width: 15px; + height: 15px; + background: #B71C1C; + color: #FFEBEE; + font-weight: bolder; + padding: 4px; + vertical-align: middle; + border-radius: 20px; + box-sizing: border-box; + font-size: 9px; + line-height: 7px; + text-align: center; + margin-right: 5px; +} + +.action { + margin-top: 15px; +} + +.passwordRequestSuccess { + border: 1px solid green; + background-color: lightgreen; + padding: 10px; +} + +.passwordRequestFailure { + border: 1px solid orange; + background-color: 1px solid coral; + padding: 10px; +} + +.cancel { + margin: 10px 0; +} diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js new file mode 100644 index 000000000..1867b9ac2 --- /dev/null +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -0,0 +1,45 @@ +import React from 'react'; + +import {Dialog} from 'coral-ui'; +import Button from 'coral-ui/components/Button'; + +import styles from './BanUserDialog.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => { + const {userName = '', userId = '', commentId = ''} = user; + + return ( + handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}> + handleClose()}>× +
+
+

+ {lang.t('bandialog.ban_user')} +

+
+
+

+ {lang.t('bandialog.are_you_sure', userName)} +

+ + {lang.t('bandialog.note')} + +
+
+ + +
+
+
+ ); +}; + +export default BanUserDialog; diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 3481ad671..380a001c8 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -1,17 +1,20 @@ - import React from 'react'; import timeago from 'timeago.js'; +import Linkify from 'react-linkify'; + import styles from './CommentList.css'; + import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations.json'; -import Linkify from 'react-linkify'; -import {FabButton} from 'coral-ui'; + import {Icon} from 'react-mdl'; +import {FabButton, Button} from 'coral-ui'; const linkify = new Linkify(); // Render a single comment for the list export default props => { + const authorStatus = props.author.get('status'); const {comment, author} = props; const links = linkify.getMatches(comment.get('body')); @@ -28,15 +31,13 @@ export default props => { {links ? Contains Link : null}
- {props.actions.map((action, i) => canShowAction(action, comment) ? ( - props.onClickAction(props.actionsMap[action].status, comment.get('id'))} - /> - ) : null)} + {props.actions.map((action, i) => getActionButton(action, i, props))}
+
+ {authorStatus === 'banned' ? + {lang.t('comment.banned_user')} : null} +
@@ -49,15 +50,33 @@ export default props => { ); }; -// Check if an action can be performed over a comment -const canShowAction = (action, comment) => { - const status = comment.get('status'); - const flagged = comment.get('flagged'); +// Get the button of the action performed over a comment if any +const getActionButton = (action, i, props) => { + const status = props.comment.get('status'); + const flagged = props.comment.get('flagged'); + const banned = (props.author.get('status') === 'banned'); if (action === 'flag' && (status || flagged === true)) { - return false; + return null; } - return true; + if (action === 'ban') { + return ( + + ); + } + return ( + props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))} + /> + ); }; const linkStyles = { diff --git a/client/coral-admin/src/components/CommentList.css b/client/coral-admin/src/components/CommentList.css index 2c58c81cf..fddee7553 100644 --- a/client/coral-admin/src/components/CommentList.css +++ b/client/coral-admin/src/components/CommentList.css @@ -122,7 +122,6 @@ } - .hasLinks { color: #f00; text-align: right; @@ -133,3 +132,14 @@ margin-right: 5px; } } + +.banned { + color: #f00; + text-align: left; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 40b99b892..b4547335e 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -9,7 +9,8 @@ import Comment from 'components/Comment'; const actions = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, - 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'} + 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, + 'ban': {status: 'banned', icon: 'not interested'} }; // Renders a comment list and allow performing actions @@ -19,6 +20,7 @@ export default class CommentList extends React.Component { this.state = {active: null}; this.onClickAction = this.onClickAction.bind(this); + this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this); } // remove key handlers before leaving @@ -99,7 +101,8 @@ export default class CommentList extends React.Component { // If we are performing an action over a comment (aka removing from the list) we need to select a new active. // TODO: In the future this can be improved and look at the actual state to // resolve since the content of the list could change externally. For now it works as expected - onClickAction (action, id) { + onClickAction (action, id, author_id) { + // activate the next comment if (id === this.state.active) { const {commentIds} = this.props; if (commentIds.last() === this.state.active) { @@ -108,7 +111,11 @@ export default class CommentList extends React.Component { this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))}); } } - this.props.onClickAction(action, id); + this.props.onClickAction(action, id, author_id); + } + + onClickShowBanDialog(userId, userName, commentId) { + this.props.onClickShowBanDialog(userId, userName, commentId); } render () { @@ -125,6 +132,7 @@ export default class CommentList extends React.Component { key={index} index={index} onClickAction={this.onClickAction} + onClickShowBanDialog={this.onClickShowBanDialog} actions={this.props.actions} actionsMap={actions} isActive={commentId === active} diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js new file mode 100644 index 000000000..856f619d0 --- /dev/null +++ b/client/coral-admin/src/constants/comments.js @@ -0,0 +1,3 @@ +export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; +export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; +export const USER_BAN_SUCESS = 'USER_BAN_SUCESS'; diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index 2ea77ea77..e3fd88a71 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -4,3 +4,4 @@ export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE'; export const SORT_UPDATE = 'SORT_UPDATE'; export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE'; export const SET_ROLE = 'SET_ROLE'; +export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS'; diff --git a/client/coral-admin/src/containers/Community/Community.css b/client/coral-admin/src/containers/Community/Community.css index 63148da7e..b19d0261d 100644 --- a/client/coral-admin/src/containers/Community/Community.css +++ b/client/coral-admin/src/containers/Community/Community.css @@ -1,7 +1,3 @@ -.dataTable { - width: 100%; -} - .roleButton { display: block; } @@ -9,14 +5,13 @@ .searchInput { display: block; padding-left: 40px; - /*border: none;*/ + width: auto; } .searchBox { - /*border: 1px solid rgba(0,0,0,.12);*/ background: white; } .email { display: block; -} \ No newline at end of file +} diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index f8c24fd3a..e798266f0 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -20,6 +20,10 @@ const tableHeaders = [ title: lang.t('community.account_creation_date'), field: 'created_at' }, + { + title: lang.t('community.status'), + field: 'status' + }, { title: lang.t('community.newsroom_role'), field: 'role' @@ -30,7 +34,7 @@ const Community = ({isFetching, commenters, ...props}) => { const hasResults = !isFetching && !!commenters.length; return ( - +
- + { isFetching && } { !hasResults && } { hasResults && diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index 89737e33e..1d81b1b2e 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -4,7 +4,7 @@ import {SelectField, Option} from 'react-mdl-selectfield'; import styles from './Community.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations'; -import {setRole} from '../../actions/community'; +import {setRole, setCommenterStatus} from '../../actions/community'; const lang = new I18n(translations); @@ -19,6 +19,10 @@ class Table extends Component { this.props.dispatch(setRole(id, role)); } + onCommenterStatusChange (id, status) { + this.props.dispatch(setCommenterStatus(id, status)); + } + render () { const {headers, commenters, onHeaderClickHandler} = this.props; @@ -46,6 +50,14 @@ class Table extends Component { {row.created_at} + + this.onCommenterStatusChange(row.id, status)}> + + + + () => { + const moderation = mod === 'pre' ? 'post' : 'pre'; + updateSettings({moderation}); +}; + +const updateInfoBoxEnable = (updateSettings, infoBox) => () => { + const infoBoxEnable = !infoBox; + updateSettings({infoBoxEnable}); +}; + +const updateInfoBoxContent = (updateSettings) => (event) => { + const infoBoxContent = event.target.value; + updateSettings({infoBoxContent}); +}; + +const updateClosedMessage = (updateSettings) => (event) => { + const closedMessage = event.target.value; + updateSettings({closedMessage}); +}; + +const CommentSettings = (props) => + + + + + {lang.t('configure.enable-pre-moderation')} + + + + + + + {lang.t('configure.include-comment-stream')} +

+ {lang.t('configure.include-comment-stream-desc')} +

+
+
+ + + + + + + + {lang.t('configure.closed-comments-desc')} + + + +
; + +export default CommentSettings; + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Configure/Configure.css b/client/coral-admin/src/containers/Configure/Configure.css index 8d1fc49a2..388813a64 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -45,6 +45,10 @@ display: block; } +.changedSave { + background-color:#4caf50; +} + .copiedText { color: #008000; float: right; @@ -69,6 +73,19 @@ letter-spacing: 0.03em; } +#bannedWordlist { + width: 100%; + padding: 10px; +} + +.bannedWordHeader { + font-weight: bold; + font-size:18px; + margin-bottom:3px; +} + + + .hidden { display: none; } diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index a9e2f3ef2..db3bf6f20 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -5,126 +5,94 @@ import { List, ListItem, ListItemContent, - ListItemAction, - Textfield, - Checkbox, Button, Icon } from 'react-mdl'; import styles from './Configure.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; +import EmbedLink from './EmbedLink'; +import CommentSettings from './CommentSettings'; +import Wordlist from './Wordlist'; class Configure extends React.Component { constructor (props) { super(props); - this.state = {activeSection: 'comments', copied: false}; - - this.copyToClipBoard = this.copyToClipBoard.bind(this); - - // Update settings - this.updateModeration = this.updateModeration.bind(this); - // InfoBox has two settings. Enable or not and the content of it if it is enable. - this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this); - this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this); - - this.saveSettings = this.saveSettings.bind(this); + this.state = { + activeSection: 'comments', + wordlist: [], + changed: false + }; } - componentWillMount () { + componentWillMount = () => { this.props.dispatch(fetchSettings()); } - updateModeration () { - const moderation = this.props.settings.moderation === 'pre' ? 'post' : 'pre'; - this.props.dispatch(updateSettings({moderation})); - } - - updateInfoBoxEnable () { - const infoBoxEnable = !this.props.settings.infoBoxEnable; - this.props.dispatch(updateSettings({infoBoxEnable})); - } - - updateInfoBoxContent (event) { - const infoBoxContent = event.target.value; - this.props.dispatch(updateSettings({infoBoxContent})); - } - - saveSettings () { - this.props.dispatch(saveSettingsToServer()); - } - - getCommentSettings () { - return - - - - - {lang.t('configure.enable-pre-moderation')} - - - - - - - {lang.t('configure.include-comment-stream')} -

- {lang.t('configure.include-comment-stream-desc')} -

-
-
- - - - - -
; - } - - copyToClipBoard () { - const copyTextarea = document.querySelector(`.${ styles.embedInput}`); - copyTextarea.select(); - - try { - document.execCommand('copy'); - this.setState({copied: true}); - } catch (err) { - console.error('Unable to copy', err); + componentWillUpdate = (newProps) => { + if ((!this.props.settings + || !this.props.settings.wordlist) + && newProps.settings.wordlist + && newProps.settings.wordlist.length !== 0 ) { + this.setState({wordlist: newProps.settings.wordlist.join(', ')}); } } - getEmbed () { - const embedText = `
`; - - return - -

{lang.t('configure.copy-and-paste')}

-