diff --git a/.eslintrc.json b/.eslintrc.json index d00d21106..d2356ace2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,5 +2,10 @@ "env": { "jest": true }, + "settings": { + "react": { + "version": "15.0" + } + }, "extends": "@coralproject/eslint-config-talk" } diff --git a/.nsprc b/.nsprc index 530a17a9f..8962db719 100644 --- a/.nsprc +++ b/.nsprc @@ -7,6 +7,7 @@ "https://nodesecurity.io/advisories/594", "https://nodesecurity.io/advisories/603", "https://nodesecurity.io/advisories/611", - "https://nodesecurity.io/advisories/612" + "https://nodesecurity.io/advisories/612", + "https://nodesecurity.io/advisories/654" ] } diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..2f954597a --- /dev/null +++ b/.nvmrc @@ -0,0 +1,2 @@ +8 + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..10041faa8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +overrides: +- files: "bin/cli*" + options: + parser: babylon diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 3e837aad6..85becf461 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -7,6 +7,7 @@ ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3 ONBUILD ARG TALK_THREADING_LEVEL=3 ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all ONBUILD ARG TALK_DEFAULT_LANG=en +ONBUILD ARG TALK_WHITELISTED_LANGUAGES ONBUILD ARG TALK_PLUGINS_JSON ONBUILD ARG TALK_WEBPACK_SOURCE_MAP @@ -20,4 +21,4 @@ ONBUILD COPY . /usr/src/app ONBUILD RUN cli plugins reconcile && \ yarn && \ yarn build && \ - yarn cache clean \ No newline at end of file + yarn cache clean diff --git a/app.js b/app.js index c5507da2e..b3400b939 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,6 @@ const express = require('express'); +const nunjucks = require('nunjucks'); +const cons = require('consolidate'); const trace = require('./middleware/trace'); const logging = require('./middleware/logging'); const path = require('path'); @@ -72,7 +74,26 @@ app.use( // VIEW CONFIGURATION //============================================================================== -app.set('views', path.join(__dirname, 'views')); +// configure the default views directory. +const views = path.join(__dirname, 'views'); +app.set('views', views); + +// reconfigure nunjucks. +cons.requires.nunjucks = nunjucks.configure(views, { + autoescape: true, + trimBlocks: true, + lstripBlocks: true, + watch: process.env.NODE_ENV === 'development', +}); + +// assign the nunjucks engine to .njk files. +app.engine('njk', cons.nunjucks); + +// assign the ejs engine to .ejs and .html files. +app.engine('ejs', cons.ejs); +app.engine('html', cons.ejs); + +// set .ejs as the default extension. app.set('view engine', 'ejs'); //============================================================================== diff --git a/app.json b/app.json index 6c85e3cc2..9f245105f 100644 --- a/app.json +++ b/app.json @@ -18,6 +18,10 @@ "value": "", "required": true }, + "MAILGUN_SMTP_PASSWORD": { + "value": "", + "required": true + }, "NODE_ENV": "production", "REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" diff --git a/bin/cli-assets b/bin/cli-assets index 5c1afb741..8242a02d6 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -7,7 +7,7 @@ const util = require('./util'); const program = require('commander'); const parseDuration = require('ms'); -const Table = require('cli-table'); +const Table = require('cli-table2'); const AssetModel = require('../models/asset'); const CommentModel = require('../models/comment'); const AssetsService = require('../services/assets'); diff --git a/bin/cli-plugins b/bin/cli-plugins index f2e76de21..85132c39d 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -329,8 +329,7 @@ async function createSeedPlugin() { if (answers.addPluginsJson) { const pluginsJson = path.resolve(__dirname, '..', 'plugins.json'); - fs - .readJson(pluginsJson) + fs.readJson(pluginsJson) .then(j => { // This is a client-side plugin, let's push this. if (answers.client) { diff --git a/bin/cli-token b/bin/cli-token index b131728e6..2590ec106 100755 --- a/bin/cli-token +++ b/bin/cli-token @@ -8,7 +8,7 @@ const util = require('./util'); const program = require('commander'); const mongoose = require('../services/mongoose'); const TokensService = require('../services/tokens'); -const Table = require('cli-table'); +const Table = require('cli-table2'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); @@ -49,7 +49,10 @@ async function revokeToken(tokenID) { async function createToken(userID, tokenName) { try { - let { pat: { id }, jwt } = await TokensService.create(userID, tokenName); + let { + pat: { id }, + jwt, + } = await TokensService.create(userID, tokenName); console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`); diff --git a/bin/cli-users b/bin/cli-users index bf34cce97..743119a0a 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -8,7 +8,7 @@ const util = require('./util'); const program = require('commander'); const inquirer = require('inquirer'); const { stripIndent } = require('common-tags'); -const Table = require('cli-table'); +const Table = require('cli-table2'); // Make things colorful! require('colors'); diff --git a/client/coral-admin/src/actions/rejectUsernameDialog.js b/client/coral-admin/src/actions/rejectUsernameDialog.js new file mode 100644 index 000000000..946b36ef7 --- /dev/null +++ b/client/coral-admin/src/actions/rejectUsernameDialog.js @@ -0,0 +1,14 @@ +import { + SHOW_REJECT_USERNAME_DIALOG, + HIDE_REJECT_USERNAME_DIALOG, +} from '../constants/rejectUsernameDialog'; + +export const showRejectUsernameDialog = ({ userId, username }) => ({ + type: SHOW_REJECT_USERNAME_DIALOG, + userId, + username, +}); + +export const hideRejectUsernameDialog = () => ({ + type: HIDE_REJECT_USERNAME_DIALOG, +}); diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js index 81b857a13..52370f9a7 100644 --- a/client/coral-admin/src/components/ActionsMenu.js +++ b/client/coral-admin/src/components/ActionsMenu.js @@ -76,7 +76,7 @@ ActionsMenu.propTypes = { icon: PropTypes.string, children: PropTypes.node, className: PropTypes.string, - label: PropTypes.string, + label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), buttonClassNames: PropTypes.string, }; diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js index 7370ba466..54816147a 100644 --- a/client/coral-admin/src/components/ActionsMenuItem.js +++ b/client/coral-admin/src/components/ActionsMenuItem.js @@ -15,7 +15,7 @@ const ActionsMenuItem = props => ( ActionsMenuItem.propTypes = { className: PropTypes.string, - children: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), }; export default ActionsMenuItem; diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js index 99bd61696..e6bcb3b89 100644 --- a/client/coral-admin/src/components/AdminLogin.js +++ b/client/coral-admin/src/components/AdminLogin.js @@ -5,6 +5,7 @@ import styles from './NotFound.css'; import { Button, TextField, Alert, Success } from 'coral-ui'; import Recaptcha from 'react-recaptcha'; import cn from 'classnames'; +import t from 'coral-framework/services/i18n'; class AdminLogin extends React.Component { constructor(props) { @@ -41,13 +42,13 @@ class AdminLogin extends React.Component { {errorMessage && {errorMessage}} this.setState({ email: e.target.value })} /> this.setState({ password: e.target.value })} type="password" @@ -60,10 +61,10 @@ class AdminLogin extends React.Component { full onClick={this.handleSignIn} > - Sign In + {t('login.sign_in_button')}

- Forgot your password?{' '} + {t('login.forgot_password')}{' '} - Request a new one. + {t('login.request_passowrd')}

{loginMaxExceeded && ( @@ -95,14 +96,14 @@ class AdminLogin extends React.Component { > {this.props.passwordRequestSuccess}{' '} - Sign in + {t('login.sign_in')}

) : (
this.setState({ email: e.target.value })} /> @@ -112,17 +113,15 @@ class AdminLogin extends React.Component { full onClick={this.handleRequestPassword} > - Reset Password + {t('login.reset_password')} ); return (
-

Team sign in

-

- Sign in to interact with your community. -

+

{t('login.team_sign_in')}

+

{t('login.sign_in_message')}

{this.state.requestPassword ? requestPasswordForm : signInForm}
diff --git a/client/coral-admin/src/components/ApproveButton.js b/client/coral-admin/src/components/ApproveButton.js index 884ac3da5..96dbdd375 100644 --- a/client/coral-admin/src/components/ApproveButton.js +++ b/client/coral-admin/src/components/ApproveButton.js @@ -14,7 +14,8 @@ const ApproveButton = ({ active, minimal, onClick, className, disabled }) => { className={cn( styles.root, { [styles.minimal]: minimal, [styles.active]: active }, - className + className, + 'talk-admin-approve-button' )} onClick={onClick} disabled={disabled || active} diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js index 430ee2fe4..fd8636320 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -19,7 +19,9 @@ class BanUserDialog extends React.Component { } handleMessageChange = e => { - const { value: message } = e; + const { + target: { value: message }, + } = e; this.setState({ message }); }; @@ -30,6 +32,12 @@ class BanUserDialog extends React.Component { }); }; + handlePerform = () => { + this.props.onPerform({ + message: this.state.message, + }); + }; + renderStep0() { const { onCancel, username, info } = this.props; @@ -63,7 +71,7 @@ class BanUserDialog extends React.Component { } renderStep1() { - const { onCancel, onPerform } = this.props; + const { onCancel } = this.props; const { message } = this.state; return ( @@ -95,7 +103,7 @@ class BanUserDialog extends React.Component {

- Go back to{' '} + {t('login.go_back')}{' '} - Sign In + {t('login.sign_in')} .

diff --git a/client/coral-admin/src/components/Header.js b/client/coral-admin/src/components/Header.js index f0bb68c0c..fadd33c15 100644 --- a/client/coral-admin/src/components/Header.js +++ b/client/coral-admin/src/components/Header.js @@ -83,7 +83,7 @@ const CoralHeader = ({ target="_blank" rel="noopener noreferrer" > - View latest version + {t('configure.view_last_version')} @@ -92,7 +92,7 @@ const CoralHeader = ({ target="_blank" rel="noopener noreferrer" > - Report a bug or give feedback + {t('configure.report_bug_or_feedback')} {currentUser && ( diff --git a/client/coral-admin/src/components/KarmaTooltip.css b/client/coral-admin/src/components/KarmaTooltip.css new file mode 100644 index 000000000..27afe7b38 --- /dev/null +++ b/client/coral-admin/src/components/KarmaTooltip.css @@ -0,0 +1,103 @@ +.karmaTooltip { + position: relative; + display: inline-block; + margin: 2px 4px 0; +} + +.icon { + font-size: 16px; + color: #0D5B8F; + user-select: none; + -webkit-tap-highlight-color:rgba(0,0,0,0); + + > i { + vertical-align: baseline; + } +} + +.icon:hover { + cursor: pointer; +} + +.menu { + background-color: white; + border: solid 1px #999; + border-radius: 3px; + padding: 10px; + position: absolute; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); + z-index: 10; + top: 32px; + left: -100px; + width: 150px; + text-align: left; + color: #616161; +} + +.menu::before{ + content: ''; + border: 10px solid transparent; + border-top-color: #999; + position: absolute; + left: 96px; + top: -20px; + transform: rotate(180deg); +} + +.menu::after{ + content: ''; + border: 10px solid transparent; + border-top-color: white; + position: absolute; + left: 96px; + top: -19px; + transform: rotate(180deg); +} + +.menu ul { + list-style: none; + padding: 0; + + li { + display: flex; + justify-content: space-between; + margin: 5px 0; + } +} + +.label { + padding: 4px 5px; + border-radius: 3px; + color: #fff; + font-weight: 400; + text-align: center; + font-size: .9em; + line-height: normal; + letter-spacing: .4px; + min-width: 25px; + display: block; + + /* &.reliable { background-color: #03AB61; } */ + /* &.neutral { background-color: #616161; } */ + &.unreliable { background-color: #F44336; } +} + +.descriptionList { + padding: 0; + margin: 0; + list-style: none; +} + +.strongItem { + margin-right: 3px; +} + +.descriptionItem { + font-size: 0.9em; +} + +.link { + color: #2B7EB5; + text-decoration: underline; + display: block; +} diff --git a/client/coral-admin/src/components/KarmaTooltip.js b/client/coral-admin/src/components/KarmaTooltip.js new file mode 100644 index 000000000..d27597325 --- /dev/null +++ b/client/coral-admin/src/components/KarmaTooltip.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import { Icon } from 'coral-ui'; +import styles from './KarmaTooltip.css'; +import ClickOutside from 'coral-framework/components/ClickOutside'; +import t from 'coral-framework/services/i18n'; + +const initialState = { menuVisible: false }; + +class KarmaTooltip extends React.Component { + static propTypes = { + thresholds: PropTypes.shape({ + reliable: PropTypes.number.isRequired, + unreliable: PropTypes.number.isRequired, + }).isRequired, + }; + + state = initialState; + + toogleMenu = () => { + this.setState({ menuVisible: !this.state.menuVisible }); + }; + + hideMenu = () => { + this.setState({ menuVisible: false }); + }; + + render() { + const { + thresholds: { unreliable }, + } = this.props; + const { menuVisible } = this.state; + + return ( + +
+ + + + + {menuVisible && ( +
+ {t('user_detail.user_karma_score')} +
    + {/* NOTE: we may display this data in the future, keeping around for that eventuality */} + {/*
  • + Reliable{' '} + + ≥ {reliable} + +
  • +
  • + Neutral{' '} + + < {reliable}, > {unreliable} + +
  • */} +
  • + {t('user_detail.unreliable')}{' '} + + ≤ {unreliable} + +
  • +
+ + {t('user_detail.learn_more')} + +
+ )} +
+
+ ); + } +} + +export default KarmaTooltip; diff --git a/client/coral-admin/src/components/Login.js b/client/coral-admin/src/components/Login.js index cc5ec9436..2f3f9a975 100644 --- a/client/coral-admin/src/components/Login.js +++ b/client/coral-admin/src/components/Login.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import styles from './Login.css'; import Layout from 'coral-admin/src/components/Layout'; import cn from 'classnames'; +import t from 'coral-framework/services/i18n'; class LoginContainer extends Component { renderForm() { @@ -19,8 +20,8 @@ class LoginContainer extends Component { return (
-

Team sign in

-

Sign in to interact with your community.

+

{t('login.team_sign_in')}

+

{t('login.sign_in_message')}

{this.renderForm()}
diff --git a/client/coral-admin/src/components/RejectButton.js b/client/coral-admin/src/components/RejectButton.js index 69e1047e6..2aabff007 100644 --- a/client/coral-admin/src/components/RejectButton.js +++ b/client/coral-admin/src/components/RejectButton.js @@ -14,7 +14,8 @@ const RejectButton = ({ active, minimal, onClick, className, disabled }) => { className={cn( styles.root, { [styles.minimal]: minimal, [styles.active]: active }, - className + className, + 'talk-admin-reject-button' )} onClick={onClick} disabled={disabled || active} diff --git a/client/coral-admin/src/components/RejectUsernameDialog.css b/client/coral-admin/src/components/RejectUsernameDialog.css new file mode 100644 index 000000000..57c9ca24a --- /dev/null +++ b/client/coral-admin/src/components/RejectUsernameDialog.css @@ -0,0 +1,80 @@ +.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: 400px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; +} + +.header { + color: black; + font-size: 1.5em; + font-weight: 500; + margin: 0 0 8px 0; +} + +.close { + display: block; + position: absolute; + top: 24px; + right: 20px; +} + +.closeButton { + font-size: 24px; + line-height: 14px; + color: #363636; + &:hover { + color: #6b6b6b; + } +} + +.legend { + padding: 0; + font-weight: bold; +} + +div.radioGroup { + margin-top: 6px; +} + +label.radioGroup { + + &:global(.is-checked) > :global(.mdl-radio__outer-circle), + > :global(.mdl-radio__outer-circle) { + border-color: #212121; + } + + > :global(.mdl-radio__inner-circle) { + background: #212121; + } + + > :global(.mdl-radio__label) { + font-size: 14px; + line-height: 20px; + } +} + +.messageInput { + border-radius: 3px; + width: 100%; + padding: 10px; + font-size: 14px; + box-sizing: border-box; +} + +.cancel { + margin-right: 5px; +} + +.perform { + min-width: 90px; +} + +.buttons { + margin-top: 8px; + margin-bottom: 6px; + text-align: right; +} diff --git a/client/coral-admin/src/components/RejectUsernameDialog.js b/client/coral-admin/src/components/RejectUsernameDialog.js new file mode 100644 index 000000000..b14384eb8 --- /dev/null +++ b/client/coral-admin/src/components/RejectUsernameDialog.js @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dialog, BareButton } from 'coral-ui'; +import styles from './RejectUsernameDialog.css'; +import cn from 'classnames'; +import { RadioGroup, Radio } from 'react-mdl'; +import Button from 'coral-ui/components/Button'; +import { username as flagReason } from 'coral-framework/graphql/flagReasons'; +import t from 'coral-framework/services/i18n'; + +const initialState = { reason: flagReason.offensive, message: '' }; + +class RejectUsernameDialog extends React.Component { + state = initialState; + + componentWillReceiveProps(next) { + if (this.props.open && !next.open) { + this.setState(initialState); + } + } + + handleReasonChange = event => { + this.setState({ reason: event.target.value }); + }; + + handleMessageChange = event => { + this.setState({ message: event.target.value }); + }; + + handlePerform = () => { + this.props.onPerform({ + reason: this.state.reason, + message: this.state.message, + }); + }; + + render() { + const { open, onCancel } = this.props; + const { reason, message } = this.state; + return ( + +
+ + × + +
+
+

+ {t('reject_username_dialog.title')}: {this.props.username} +

+

+ {t('reject_username_dialog.description')} +

+
+ + {t('reject_username_dialog.reason')} + + + + {t('flag_reasons.username.offensive')} + + + {t('flag_reasons.username.nolike')} + + + {t('flag_reasons.username.impersonating')} + + + {t('flag_reasons.username.spam')} + + + {t('flag_reasons.username.other')} + + + {reason === flagReason.other && ( +
+ + {t('reject_username_dialog.message')} + +