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/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/bin/cli-assets b/bin/cli-assets index 210b9f22e..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'); @@ -23,23 +23,33 @@ util.onshutdown([() => mongoose.disconnect()]); /** * Lists all the assets registered in the database. */ -async function listAssets() { +async function listAssets(opts) { try { let assets = await AssetModel.find({}).sort({ created_at: 1 }); - let table = new Table({ - head: ['ID', 'Title', 'URL'], - }); + switch (opts.format) { + case 'json': { + console.log(JSON.stringify(assets, null, 2)); + break; + } + default: { + let table = new Table({ + head: ['ID', 'Title', 'URL'], + }); - assets.forEach(asset => { - table.push([ - asset.id, - asset.title ? asset.title : '', - asset.url ? asset.url : '', - ]); - }); + assets.forEach(asset => { + table.push([ + asset.id, + asset.title ? asset.title : '', + asset.url ? asset.url : '', + ]); + }); + + console.log(table.toString()); + break; + } + } - console.log(table.toString()); util.shutdown(); } catch (e) { console.error(e); @@ -49,12 +59,13 @@ async function listAssets() { async function refreshAssets(ageString) { try { - const now = new Date().getTime(); - const ageMs = parseDuration(ageString); - const age = new Date(now - ageMs); + const query = AssetModel.find({}, { id: 1 }); + if (ageString) { + // An age was specified, so filter only those assets. + const ageMs = parseDuration(ageString); + const age = new Date(Date.now() - ageMs); - let assets = await AssetModel.find( - { + query.merge({ $or: [ { scraped: { @@ -65,16 +76,28 @@ async function refreshAssets(ageString) { scraped: null, }, ], - }, - { id: 1 } - ); + }); + } // Create a graph context. const ctx = Context.forSystem(); + // Load the assets. + const cursor = query.cursor(); + // Queue all the assets for scraping. - await Promise.all(assets.map(({ id }) => scraper.create(ctx, id))); - console.log('Assets were queued to be scraped'); + const promises = []; + + let asset = await cursor.next(); + while (asset) { + promises.push(scraper.create(ctx, asset.id)); + asset = await cursor.next(); + } + + await Promise.all(promises); + + console.log(`${promises.length} Assets were queued to be scraped.`); + util.shutdown(); } catch (e) { console.error(e); @@ -202,11 +225,17 @@ async function rewrite(search, replace, options) { program .command('list') + .option( + '--format ', + 'Specify the output format [table]', + /^(table|json)$/i, + 'table' + ) .description('list all the assets in the database') .action(listAssets); program - .command('refresh ') + .command('refresh [age]') .description('queues the assets that exceed the age requested') .action(refreshAssets); diff --git a/bin/cli-settings b/bin/cli-settings index 72f42672d..05c7f2888 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -18,7 +18,7 @@ async function changeOrgName() { await cache.init(); // Get the original settings. - const settings = await Settings.retrieve('organizationName'); + const settings = await Settings.select('organizationName'); const { organizationName } = await inquirer.prompt([ { diff --git a/bin/cli-token b/bin/cli-token index b131728e6..0d4dc4e10 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()]); diff --git a/bin/cli-users b/bin/cli-users index a01980a20..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'); @@ -328,7 +328,7 @@ program .action(searchUsers); program - .command('set-role ') + .command('set-role ') .description('sets the role on a user') .action(setUserRole); diff --git a/client/coral-admin/src/components/App.css b/client/coral-admin/src/components/App.css new file mode 100644 index 000000000..39c7cf942 --- /dev/null +++ b/client/coral-admin/src/components/App.css @@ -0,0 +1,11 @@ +:global { + html, body, #root, #root > div { + min-height: 100%; + } + + body { + margin: 0; + background-color: #FAFAFA; + font-family: 'Roboto', sans-serif; + } +} diff --git a/client/coral-admin/src/components/App.js b/client/coral-admin/src/components/App.js index 52535f91a..a7ab1e336 100644 --- a/client/coral-admin/src/components/App.js +++ b/client/coral-admin/src/components/App.js @@ -1,5 +1,6 @@ import React from 'react'; import ToastContainer from './ToastContainer'; +import './App.css'; import 'material-design-lite'; import AppRouter from '../AppRouter'; 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..f211a9f2c 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -19,7 +19,7 @@ class BanUserDialog extends React.Component { } handleMessageChange = e => { - const { value: message } = e; + const { target: { value: message } } = e; this.setState({ message }); }; @@ -30,6 +30,12 @@ class BanUserDialog extends React.Component { }); }; + handlePerform = () => { + this.props.onPerform({ + message: this.state.message, + }); + }; + renderStep0() { const { onCancel, username, info } = this.props; @@ -63,7 +69,7 @@ class BanUserDialog extends React.Component { } renderStep1() { - const { onCancel, onPerform } = this.props; + const { onCancel } = this.props; const { message } = this.state; return ( @@ -95,7 +101,7 @@ class BanUserDialog extends React.Component { -

- Forgot your password?{' '} - + + + {errorMessage && {errorMessage}} + + + {requireRecaptcha && ( +

+ )} + +

+ {/* TODO: translate */} + Forgot your password?{' '} + + Request a new one. + +

+ + ); } } diff --git a/client/coral-admin/src/components/UserDetail.css b/client/coral-admin/src/components/UserDetail.css index 58faee5c4..c35e4776f 100644 --- a/client/coral-admin/src/components/UserDetail.css +++ b/client/coral-admin/src/components/UserDetail.css @@ -35,44 +35,49 @@ margin-right: 20px; } +.karmaStat { + display: flex; +} + .stat:last-child { margin-right: 0px; } -.statItem, -.statReportResult { +.statItem, .statReportResult, .statKarmaResult { padding: 3px 5px; background-color: #D8D8D8; border-radius: 3px; font-weight: 500; - display: block; font-size: 0.9em; line-height: normal; letter-spacing: 0.4px; - min-width: 60px; + min-width: 25px; + display: block; } .statResult { font-size: 1.5em; padding: 5px 0; display: inline-block; + text-align: center; } -.statReportResult { +.statReportResult, .statKarmaResult { color: white; margin: 5px 0; font-weight: 400; + text-align: center; } -.statReportResult.reliable { - background-color: #749C48; +.statReportResult.reliable, .statKarmaResult.good { + background-color: #03AB61; } -.statReportResult.neutral { +.statReportResult.neutral, .statKarmaResult.neutral { background-color: #616161; } -.statReportResult.unreliable { +.statReportResult.unreliable, .statKarmaResult.bad { background-color: #F44336; } @@ -143,3 +148,7 @@ border-color: #E45241; color: white; } + +.userDetailItem { + padding: 2px 0; +} diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 5dabc5b6f..fd609373d 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -6,11 +6,7 @@ import styles from './UserDetail.css'; import UserHistory from './UserHistory'; import { Slot } from 'coral-framework/components'; import UserDetailCommentList from '../components/UserDetailCommentList'; -import { - getReliability, - isSuspended, - isBanned, -} from 'coral-framework/utils/user'; +import { isSuspended, isBanned, getKarma } from 'coral-framework/utils/user'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; import ClickOutside from 'coral-framework/components/ClickOutside'; import { @@ -25,6 +21,7 @@ import { import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import UserInfoTooltip from './UserInfoTooltip'; +import KarmaTooltip from './KarmaTooltip'; import t from 'coral-framework/services/i18n'; class UserDetail extends React.Component { @@ -79,7 +76,13 @@ class UserDetail extends React.Component { renderLoaded() { const { root, - root: { me, user, totalComments, rejectedComments }, + root: { + me, + user, + totalComments, + rejectedComments, + settings: { karmaThresholds }, + }, activeTab, selectedCommentIds, toggleSelect, @@ -177,7 +180,7 @@ class UserDetail extends React.Component {
    -
  • +
  • {t('user_detail.member_since')}: @@ -185,11 +188,24 @@ class UserDetail extends React.Component { {new Date(user.created_at).toLocaleString()}
  • - {user.profiles.map(({ id }) => ( -
  • - +
  • + + + {t('user_detail.email')}: + + {user.email}{' '} + +
  • + + {user.profiles.map(({ provider, id }) => ( +
  • + - {t('user_detail.email')}: + {capitalize(provider)} {t('user_detail.id')}: {id}{' '}
  • -
  • - - {t('user_detail.reports')} - - - {capitalize(getReliability(user.reliable.flagger))} - +
  • +
    + + {t('user_detail.karma')} + + + {user.reliable.commenterKarma} + +
    +
diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index f2ff2e932..41fb87b28 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -12,6 +12,7 @@ import CommentAnimatedEdit from './CommentAnimatedEdit'; import CommentLabels from '../containers/CommentLabels'; import ApproveButton from './ApproveButton'; import RejectButton from 'coral-admin/src/components/RejectButton'; +import CommentDeletedTombstone from './CommentDeletedTombstone'; import t, { timeago } from 'coral-framework/services/i18n'; @@ -43,6 +44,19 @@ class UserDetailComment extends React.Component { body: comment.body, }; + if (!comment.body) { + return ( +
  • + +
  • + ); + } + return (
  • { + banUser = async ({ message }) => { const { userId, commentId, @@ -21,7 +21,7 @@ class BanUserDialogContainer extends Component { setCommentStatus, hideBanUserDialog, } = this.props; - await banUser({ id: userId, message: '' }); + await banUser({ id: userId, message }); hideBanUserDialog(); if (commentId && commentStatus && commentStatus !== 'REJECTED') { await setCommentStatus({ commentId, status: 'REJECTED' }); diff --git a/client/coral-admin/src/containers/Header.js b/client/coral-admin/src/containers/Header.js index d3be66764..4b8760cc7 100644 --- a/client/coral-admin/src/containers/Header.js +++ b/client/coral-admin/src/containers/Header.js @@ -2,21 +2,20 @@ import { gql } from 'react-apollo'; import withQuery from 'coral-framework/hocs/withQuery'; import Header from '../components/Header'; import CommunityIndicator from '../routes/Community/containers/Indicator'; -import ModerationIndicator from '../routes/Moderation/containers/Indicator'; +// TODO: eventually we will readd modqueue counts +// import ModerationIndicator from '../routes/Moderation/containers/Indicator'; import { getDefinitionName } from 'coral-framework/utils'; export default withQuery( gql` - query TalkAdmin_Header($nullID: ID) { - ...${getDefinitionName(ModerationIndicator.fragments.root)} + query TalkAdmin_Header { ...${getDefinitionName(CommunityIndicator.fragments.root)} } - ${ModerationIndicator.fragments.root} ${CommunityIndicator.fragments.root} `, { options: { - variables: { nullID: null }, + // variables: { nullID: null }, }, } )(Header); diff --git a/client/coral-admin/src/containers/SignIn.js b/client/coral-admin/src/containers/SignIn.js index 523d81091..1af857294 100644 --- a/client/coral-admin/src/containers/SignIn.js +++ b/client/coral-admin/src/containers/SignIn.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { withSignIn } from 'coral-framework/hocs'; +import { withSignIn, withPopupAuthHandler } from 'coral-framework/hocs'; import { compose } from 'recompose'; import SignIn from '../components/SignIn'; @@ -55,4 +55,4 @@ SignInContainer.propTypes = { requireRecaptcha: PropTypes.bool.isRequired, }; -export default compose(withSignIn)(SignInContainer); +export default compose(withSignIn, withPopupAuthHandler)(SignInContainer); diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 360819dd6..e34d4df6e 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -179,12 +179,14 @@ export const withUserDetailQuery = withQuery( id username created_at + email profiles { id provider } reliable { - flagger + commenter + commenterKarma } state { status { @@ -225,6 +227,14 @@ export const withUserDetailQuery = withQuery( } ${getSlotFragmentSpreads(slots, 'user')} } + settings { + karmaThresholds { + comment { + reliable + unreliable + } + } + } me { id } diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 3874e6c94..6a1b06a6e 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -24,6 +24,25 @@ const userRoleFragment = gql` } `; +/** + * calculateReliability will determine the reliability of a karma score based on + * the settings for the karma type. + * + * @param {Number} karma - the current karma value/score for the given user + * @param {Object} thresholds - the karma thresholds to base the karma computation on + */ +const calculateReliability = (karma, { reliable, unreliable }) => { + if (karma >= reliable) { + return true; + } + + if (karma <= unreliable) { + return false; + } + + return null; +}; + export default { mutations: { SetUserRole: ({ variables: { id, role } }) => ({ @@ -156,7 +175,9 @@ export default { } const updated = update(prev, { users: { - nodes: { $apply: nodes => nodes.filter(node => node.id !== id) }, + nodes: { + $apply: nodes => nodes.filter(node => node.id !== id), + }, }, }); return updated; @@ -185,7 +206,9 @@ export default { const updated = update(prev, { ...decrement, flaggedUsers: { - nodes: { $apply: nodes => nodes.filter(node => node.id !== id) }, + nodes: { + $apply: nodes => nodes.filter(node => node.id !== id), + }, }, }); return updated; @@ -295,12 +318,38 @@ export default { updateQueries: { CoralAdmin_UserDetail: prev => { const increment = { + user: { + reliable: { + commenter: { + $set: calculateReliability( + prev.user.reliable.commenterKarma - 1, + prev.settings.karmaThresholds.comment + ), + }, + commenterKarma: { + $apply: count => count - 1, + }, + }, + }, rejectedComments: { $apply: count => (count < prev.totalComments ? count + 1 : count), }, }; const decrement = { + user: { + reliable: { + commenter: { + $set: calculateReliability( + prev.user.reliable.commenterKarma + 1, + prev.settings.karmaThresholds.comment + ), + }, + commenterKarma: { + $apply: count => count + 1, + }, + }, + }, rejectedComments: { $apply: count => (count > 0 ? count - 1 : 0), }, diff --git a/client/coral-admin/src/routes/Community/components/People.css b/client/coral-admin/src/routes/Community/components/People.css index 669da480d..ac565c8f8 100644 --- a/client/coral-admin/src/routes/Community/components/People.css +++ b/client/coral-admin/src/routes/Community/components/People.css @@ -133,6 +133,7 @@ th.header:nth-child(2), th.header:nth-child(3) { .roleDropdown { width: 150px; + text-align: left; } .roleOption { diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 5645f88f1..d152f83e3 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -130,7 +130,9 @@ class People extends React.Component { {user.username} - {user.profiles.map(({ id }) => id)} + {user.email + ? user.email + : user.profiles.map(p => p.id).join(', ')} @@ -200,7 +202,7 @@ class People extends React.Component { { + const updater = { + disableCommenting: { + $set: !this.props.settings.disableCommenting, + }, + }; + this.props.updatePending({ updater }); + }; + + updateDisableCommentingMessage = value => { + const updater = { disableCommentingMessage: { $set: value } }; + this.props.updatePending({ updater }); + }; + updateAutoClose = () => { const updater = { autoCloseStream: { $set: !this.props.settings.autoCloseStream }, @@ -192,6 +206,25 @@ class StreamSettings extends React.Component {   {t('configure.edit_comment_timeframe_text_post')} + +

    {t('configure.disable_commenting_desc')}

    +
    + +
    +
    + +
  • + ); + } + return (
  • { - return props.root[`${props.activeTab}Count`]; - }; - moderate = accept => { const { acceptComment, @@ -139,12 +135,14 @@ class Moderation extends Component { const comments = root[activeTab]; - const activeTabCount = this.getActiveTabCount(); const menuItems = Object.keys(queueConfig).map(queue => ({ key: queue, name: queueConfig[queue].name, icon: queueConfig[queue].icon, - count: root[`${queue}Count`], + indicator: + ['premod', 'reported'].includes(queue) && root[queue].nodes.length > 0, + // TODO: Eventually we'll reintroduce counting + // count: root[`${props.queue}Count`] })); const slotPassthrough = { @@ -189,7 +187,6 @@ class Moderation extends Component { loadMore={this.loadMore} commentBelongToQueue={this.props.commentBelongToQueue} isLoadingMore={this.state.isLoadingMore} - commentCount={activeTabCount} currentUserId={this.props.currentUser.id} viewUserDetail={viewUserDetail} selectCommentId={props.selectCommentId} diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js index 75a274eb3..5300ce3eb 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import CountBadge from '../../../components/CountBadge'; +import Indicator from '../../../components/Indicator'; import styles from './ModerationMenu.css'; import { Icon } from 'coral-ui'; import { Link } from 'react-router'; @@ -24,6 +24,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => { > {items.map(queue => ( { activeClassName={styles.active} > {queue.name}{' '} - + {queue.indicator && } ))} diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index e47a162ef..bf7fa327d 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -15,6 +15,7 @@ import { } from 'react-virtualized'; import throttle from 'lodash/throttle'; import key from 'keymaster'; +import cn from 'classnames'; const hasComment = (nodes, id) => nodes.some(node => node.id === id); @@ -204,7 +205,7 @@ class ModerationQueue extends React.Component { } componentDidUpdate(prev) { - const { commentCount, selectedCommentId } = this.props; + const { selectedCommentId, hasNextPage } = this.props; const switchedToMultiMode = prev.singleView && !this.props.singleView; const switchedMode = prev.singleView !== this.props.singleView; @@ -212,7 +213,6 @@ class ModerationQueue extends React.Component { prev.selectedCommentId !== selectedCommentId && selectedCommentId; const moderatedLastComment = prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0; - const hasMoreComment = commentCount > 0; if (switchedToMultiMode) { // Reflow virtual list. @@ -223,7 +223,7 @@ class ModerationQueue extends React.Component { this.scrollToSelectedComment(); } - if (moderatedLastComment && hasMoreComment) { + if (moderatedLastComment && hasNextPage) { this.props.loadMore(); } } @@ -240,10 +240,7 @@ class ModerationQueue extends React.Component { const index = view.findIndex( ({ id }) => id === this.props.selectedCommentId ); - if ( - index === view.length - 1 && - this.getCommentCountWithoutDagling() !== this.props.commentCount - ) { + if (index === view.length - 1 && this.props.hasNextPage) { await this.props.loadMore(); this.selectDown(); return; @@ -384,6 +381,11 @@ class ModerationQueue extends React.Component { ...props } = this.props; + const rootClassName = cn( + styles.root, + `talk-admin-moderate-queue-${this.props.activeTab}` + ); + if (comments.length === 0) { return (
    @@ -405,7 +407,7 @@ class ModerationQueue extends React.Component { const comment = comments[index]; return ( -
    +
    +
    this.viewNewComments()} count={comments.length - view.length} @@ -467,7 +469,6 @@ ModerationQueue.propTypes = { acceptComment: PropTypes.func.isRequired, commentBelongToQueue: PropTypes.func.isRequired, cleanUpQueue: PropTypes.func.isRequired, - commentCount: PropTypes.number.isRequired, loadMore: PropTypes.func.isRequired, singleView: PropTypes.bool, isLoadingMore: PropTypes.bool, diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index f35255732..e3f35081c 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -314,11 +314,11 @@ class ModerationContainer extends Component { const currentQueueConfig = Object.assign({}, this.props.queueConfig); - if (premodEnabled && root.newCount === 0) { + if (premodEnabled && root.new.nodes.length === 0) { delete currentQueueConfig.new; } - if (!premodEnabled && root.premodCount === 0) { + if (!premodEnabled && root.premod.nodes.length === 0) { delete currentQueueConfig.premod; } @@ -402,7 +402,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql` const LOAD_MORE_QUERY = gql` query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { - comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) { + comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags, excludeDeleted: true}) { nodes { ...${getDefinitionName(Comment.fragments.comment)} } @@ -432,6 +432,7 @@ const withModQueueQuery = withQuery( ${Object.keys(queueConfig).map( queue => ` ${queue}: comments(query: { + excludeDeleted: true, statuses: ${ queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` @@ -455,9 +456,14 @@ const withModQueueQuery = withQuery( } ` )} - ${Object.keys(queueConfig).map( + ${ + '' + /* + TODO: eventually we'll reintroduce counting.. + Object.keys(queueConfig).map( queue => ` ${queue}Count: commentCount(query: { + excludeDeleted: true, statuses: ${ queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` @@ -476,7 +482,8 @@ const withModQueueQuery = withQuery( asset_id: $asset_id, }) ` - )} + )*/ + } asset(id: $asset_id) @skip(if: $allAssets) { id title diff --git a/client/coral-admin/src/routes/Stories/components/Stories.css b/client/coral-admin/src/routes/Stories/components/Stories.css index 28f67151d..76c3e8e7e 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.css +++ b/client/coral-admin/src/routes/Stories/components/Stories.css @@ -92,9 +92,6 @@ .statusDropdown { width: 150px; -} - -.statusDropdownOption { - min-width: 100px; + text-align: left; } diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index ad2c7c5fc..fa0e33db5 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -22,20 +22,12 @@ class Stories extends Component { const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now()); return ( this.props.onStatusChange(value, id)} > - ); }; diff --git a/client/coral-auth-callback/src/index.js b/client/coral-auth-callback/src/index.js index 41a83dcd2..7bf75e825 100644 --- a/client/coral-auth-callback/src/index.js +++ b/client/coral-auth-callback/src/index.js @@ -3,32 +3,36 @@ import { getStaticConfiguration } from 'coral-framework/services/staticConfigura import { createPostMessage } from 'coral-framework/services/postMessage'; document.addEventListener('DOMContentLoaded', () => { - try { - const staticConfig = getStaticConfiguration(); - const { STATIC_ORIGIN: origin } = staticConfig; - const postMessage = createPostMessage(origin); + const staticConfig = getStaticConfiguration(); + const { STATIC_ORIGIN: origin } = staticConfig; + const postMessage = createPostMessage(origin); - // Get the auth element and parse it as JSON by decoding it. - const auth = document.getElementById('auth'); - const doc = document.implementation.createHTMLDocument(''); - doc.body.innerHTML = auth.innerText; + // Get the auth element and parse it as JSON by decoding it. + const auth = document.getElementById('auth'); + const doc = document.implementation.createHTMLDocument(''); + doc.body.innerHTML = auth.innerText; - // Auth state is contained within the node. - const { err, data } = JSON.parse(doc.body.textContent); - if (err) { - // TODO: send back the error message. - console.error(err); + // Auth state is contained within the node. + const { err, data } = JSON.parse(doc.body.textContent); + if (err) { + const errDiv = document.createElement('div'); + if (err.message) { + errDiv.innerText = `${err.name}: ${err.message}`; } else { - // The data will contain a user and a token. - const { user, token } = data; - - // Send the state back. - postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token }); + errDiv.innerText = JSON.stringify(err); } - } finally { - // Always close the window. - setTimeout(() => { - window.close(); - }, 50); + document.body.appendChild(errDiv); + throw err; } + + // The data will contain a user and a token. + const { user, token } = data; + + // Send the state back. + postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token }); + + // Close the window when all went well. + setTimeout(() => { + window.close(); + }, 50); }); diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index a4c6bcd39..a2718e49d 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -8,6 +8,14 @@ import reducers from './reducers'; import TalkProvider from 'coral-framework/components/TalkProvider'; import pluginsConfig from 'pluginsConfig'; +// Resolves touch handling issues encountered on IOS Safari under certain +// circumstances. It may be related to issues reported here: +// +// https://stackoverflow.com/questions/12363742/touchstart-event-is-not-firing-inside-iframe-ios-6 +// +// Further details: https://www.pivotaltracker.com/story/show/157794038 +document.body.addEventListener('touchstart', () => {}); + async function main() { const context = await createContext({ reducers, diff --git a/client/coral-embed-stream/src/tabs/profile/components/Comment.css b/client/coral-embed-stream/src/tabs/profile/components/Comment.css index 9004a1e6c..c2bf841d5 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/Comment.css +++ b/client/coral-embed-stream/src/tabs/profile/components/Comment.css @@ -31,6 +31,7 @@ font-weight: bold; font-size: 12px; color: #757575; + cursor: pointer; } .commentSummary { diff --git a/client/coral-embed-stream/src/tabs/profile/components/Comment.js b/client/coral-embed-stream/src/tabs/profile/components/Comment.js index 171172dab..badeb4262 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/profile/components/Comment.js @@ -11,16 +11,6 @@ import { getTotalReactionsCount } from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; class Comment extends React.Component { - goToStory = () => { - this.props.navigate(this.props.comment.asset.url); - }; - - goToConversation = () => { - this.props.navigate( - `${this.props.comment.asset.url}?commentId=${this.props.comment.id}` - ); - }; - render() { const { comment, root } = this.props; const reactionCount = getTotalReactionsCount(comment.action_summaries); @@ -76,8 +66,8 @@ class Comment extends React.Component {
    {t('common.story')}:{' '} {comment.asset.title ? comment.asset.title : comment.asset.url} @@ -87,7 +77,13 @@ class Comment extends React.Component {
    • - + {t('view_conversation')} diff --git a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js index 9aa2b8a4c..eae72c749 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js @@ -214,7 +214,7 @@ AllCommentsPane.propTypes = { asset: PropTypes.object, currentUser: PropTypes.object, postFlag: PropTypes.func, - postDontAgree: PropTypes.func, + postDontAgree: PropTypes.func.isRequired, loadNewReplies: PropTypes.func, deleteAction: PropTypes.func, showSignInDialog: PropTypes.func, diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js index 8b1524839..63740764f 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -184,7 +184,7 @@ export default class Comment extends React.Component { maxCharCount: PropTypes.number, root: PropTypes.object, loadMore: PropTypes.func, - postDontAgree: PropTypes.func, + postDontAgree: PropTypes.func.isRequired, animateEnter: PropTypes.bool, commentClassNames: PropTypes.array, comment: PropTypes.object.isRequired, @@ -410,6 +410,7 @@ export default class Comment extends React.Component { charCountEnable, showSignInDialog, liveUpdates, + postDontAgree, emit, } = this.props; return ( @@ -440,6 +441,7 @@ export default class Comment extends React.Component { key={reply.id} comment={reply} emit={emit} + postDontAgree={postDontAgree} /> ); })} @@ -743,10 +745,21 @@ export default class Comment extends React.Component { const id = `c_${comment.id}`; + // props that are passed down the slots. + const slotPassthrough = { + action: 'deleted', + comment, + }; + return (
      {isCommentDeleted(comment) ? ( - + ) : (
      {this.renderComment()} diff --git a/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js index 95184dffd..e08f45d95 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js +++ b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js @@ -39,6 +39,7 @@ class CommentTombstone extends React.Component { CommentTombstone.propTypes = { action: PropTypes.string, + comment: PropTypes.object, onUndo: PropTypes.func, }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index cc4e8e939..a07f8ff8b 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -4,6 +4,7 @@ import StreamError from './StreamError'; import Comment from '../containers/Comment'; import BannedAccount from '../../../components/BannedAccount'; import ChangeUsername from '../containers/ChangeUsername'; +import Markdown from 'coral-framework/components/Markdown'; import Slot from 'coral-framework/components/Slot'; import InfoBox from './InfoBox'; import { can } from 'coral-framework/services/perms'; @@ -181,7 +182,9 @@ class Stream extends React.Component { setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} - disableReply={asset.isClosed} + disableReply={ + asset.isClosed || asset.settings.disableCommenting + } postComment={postComment} currentUser={currentUser} postFlag={postFlag} @@ -215,7 +218,7 @@ class Stream extends React.Component { currentUser, } = this.props; const { keepCommentBox } = this.state; - const open = !asset.isClosed; + const open = !(asset.isClosed || asset.settings.disableCommenting); const banned = get(currentUser, 'status.banned.status'); const suspensionUntil = get(currentUser, 'status.suspension.until'); @@ -293,7 +296,13 @@ class Stream extends React.Component { )}
      ) : ( -

      {asset.settings.closedMessage}

      +
      + {asset.isClosed ? ( +

      {asset.settings.closedMessage}

      + ) : ( + + )} +
      )} diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Comment.js b/client/coral-embed-stream/src/tabs/stream/containers/Comment.js index dca7fadaa..028d6a478 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Comment.js @@ -24,6 +24,7 @@ const slots = [ 'commentAuthorName', 'commentAuthorTags', 'commentTimestamp', + 'commentTombstone', 'commentContent', ]; diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js index 17d3132ea..d6ca27558 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js @@ -265,7 +265,7 @@ StreamContainer.propTypes = { commentClassNames: PropTypes.array, setActiveStreamTab: PropTypes.func, postFlag: PropTypes.func, - postDontAgree: PropTypes.func, + postDontAgree: PropTypes.func.isRequired, deleteAction: PropTypes.func, showSignInDialog: PropTypes.func, currentUser: PropTypes.object, @@ -434,6 +434,8 @@ const fragments = { questionBoxIcon closedTimeout closedMessage + disableCommenting + disableCommentingMessage charCountEnable charCount requireEmailConfirmation diff --git a/client/coral-framework/components/Popup.js b/client/coral-framework/components/Popup.js index 53d8f7cd8..339ddb806 100644 --- a/client/coral-framework/components/Popup.js +++ b/client/coral-framework/components/Popup.js @@ -37,7 +37,8 @@ export default class Popup extends Component { this.onBlur(); }; - // Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari. + // Use `onunload` instead of `onbeforeunload` which is not supported in iOS + // Safari. this.ref.onunload = () => { this.onUnload(); @@ -46,10 +47,15 @@ export default class Popup extends Component { } this.resetCallbackInterval = setInterval(() => { - if (this.ref && this.ref.onload === null) { - clearInterval(this.resetCallbackInterval); - this.resetCallbackInterval = null; - this.setCallbacks(); + try { + if (this.ref && this.ref.onload === null) { + clearInterval(this.resetCallbackInterval); + this.resetCallbackInterval = null; + this.setCallbacks(); + } + } catch (err) { + // We could be getting a security exception here if the login page + // gets redirected to another domain to authenticate. } }, 50); diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 9caba7fb8..804e57862 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -1,5 +1,5 @@ import React from 'react'; -const PropTypes = require('prop-types'); +import PropTypes from 'prop-types'; import { ApolloProvider } from 'react-apollo'; class TalkProvider extends React.Component { diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 995ca1720..722d8ea98 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -116,7 +116,7 @@ export const withRemoveTag = withMutation( asset_id: assetId, item_type: itemType, }, - o3timisticResponse: { + optimisticResponse: { removeTag: { __typename: 'ModifyTagResponse', errors: null, diff --git a/client/coral-framework/helpers/publicPath.js b/client/coral-framework/helpers/webpackGlobals.js similarity index 57% rename from client/coral-framework/helpers/publicPath.js rename to client/coral-framework/helpers/webpackGlobals.js index 57b6b7d15..99569529b 100644 --- a/client/coral-framework/helpers/publicPath.js +++ b/client/coral-framework/helpers/webpackGlobals.js @@ -1,9 +1,9 @@ -/* global __webpack_public_path__ */ // eslint-disable-line no-unused-vars +/* global __webpack_public_path__, __webpack_nonce__ */ // eslint-disable-line no-unused-vars import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration'; // Load the static url from the static configuration. -const { STATIC_URL } = getStaticConfiguration(); +const { STATIC_URL, SCRIPT_NONCE } = getStaticConfiguration(); // Update the static url for the imported public path so dynamically imported // chunks will use the correct path as defined by the process.env.STATIC_URL @@ -14,3 +14,13 @@ const { STATIC_URL } = getStaticConfiguration(); // https://webpack.js.org/configuration/output/#output-publicpath // __webpack_public_path__ = STATIC_URL + 'static/'; + +// All dynamically included scripts that support nonce's will add this to their +// script tags. +// +// The __webpack_nonce__ can be referenced: https://webpack.js.org/guides/csp/ +// +// Pending issues: +// - https://github.com/webpack-contrib/style-loader/pull/319 +// +__webpack_nonce__ = SCRIPT_NONCE; diff --git a/client/coral-framework/hocs/withSetUsername.js b/client/coral-framework/hocs/withSetUsername.js index 05323efa1..3a8f65a22 100644 --- a/client/coral-framework/hocs/withSetUsername.js +++ b/client/coral-framework/hocs/withSetUsername.js @@ -59,6 +59,7 @@ const withSetUsername = hoistStatics(WrappedComponent => { } const changeSet = { success: false, loading: false, error }; this.setState(changeSet); + throw error; } }; diff --git a/client/coral-framework/services/i18n.js b/client/coral-framework/services/i18n.js index 03a0b4712..42a0b2030 100644 --- a/client/coral-framework/services/i18n.js +++ b/client/coral-framework/services/i18n.js @@ -1,7 +1,10 @@ -import ta from 'timeago.js'; +import { negotiateLanguages } from 'fluent-langneg/compat'; + import has from 'lodash/has'; import get from 'lodash/get'; import merge from 'lodash/merge'; +import first from 'lodash/first'; +import isUndefined from 'lodash/isUndefined'; import moment from 'moment'; import 'moment/locale/ar'; @@ -12,8 +15,8 @@ import 'moment/locale/fr'; import 'moment/locale/nl'; import 'moment/locale/pt-br'; -import { createStorage } from 'coral-framework/services/storage'; - +// timeago +import ta from 'timeago.js'; import arTA from 'timeago.js/locales/ar'; import daTA from 'timeago.js/locales/da'; import deTA from 'timeago.js/locales/de'; @@ -24,6 +27,7 @@ import pt_BRTA from 'timeago.js/locales/pt_BR'; import zh_CNTA from 'timeago.js/locales/zh_CN'; import zh_TWTA from 'timeago.js/locales/zh_TW'; +// locales import ar from '../../../locales/ar.yml'; import en from '../../../locales/en.yml'; import da from '../../../locales/da.yml'; @@ -35,8 +39,22 @@ import pt_BR from '../../../locales/pt_BR.yml'; import zh_CN from '../../../locales/zh_CN.yml'; import zh_TW from '../../../locales/zh_TW.yml'; -const defaultLanguage = process.env.TALK_DEFAULT_LANG; -const translations = { +// the list of languages that are whitelisted. If false, all languages that are +// supported by Talk will be enabled. +const whitelistedLanguages = + process.env.TALK_WHITELISTED_LANGUAGES && + process.env.TALK_WHITELISTED_LANGUAGES.split(',').map(l => l.trim()); + +// The default language. If the whitelisted languages is specified and the +// default language is not in that list, then the first language in the +// whitelisted list will be used as the default. +export const defaultLocale = whitelistedLanguages + ? !whitelistedLanguages.includes(process.env.TALK_DEFAULT_LANG) + ? whitelistedLanguages[0] + : process.env.TALK_DEFAULT_LANG + : process.env.TALK_DEFAULT_LANG; + +export const translations = { ...ar, ...en, ...da, @@ -49,84 +67,66 @@ const translations = { ...zh_TW, }; -let lang; -let timeagoInstance; +export const supportedLocales = Object.keys(translations); -function setLocale(storage, locale) { - storage.setItem('locale', locale); -} +let LOCALE; +let TIMEAGO_INSTANCE; // detectLanguage will try to get the locale from storage if available, // otherwise will try to get it from the navigator, otherwise, it will fallback // to the default language. -function detectLanguage(storage) { - try { - const lang = storage.getItem('locale') || navigator.language; - if (lang) { - return lang; - } - } catch (err) { - console.warn( - 'Error while trying to detect language, will fallback to', - err - ); - } - - console.warn('Could not detect language, will fallback to', defaultLanguage); - return defaultLanguage; -} - -// getLocale will get the users locale from the local detector and parse it to a -// format we can work with. -function getLocale(storage) { - // Get the language from the local detector. - const lang = detectLanguage(storage); - - // Some language strings come with additional subtags as defined in: - // - // https://www.ietf.org/rfc/bcp/bcp47.txt - // - // So we should strip that off if we find it. - return lang.split('-')[0]; -} +const detectLanguage = () => + first( + negotiateLanguages( + navigator.languages, + whitelistedLanguages || supportedLocales, + { + defaultLocale, + strategy: 'lookup', + } + ) + ); export function setupTranslations() { - // Setup the translation framework with the storage. - const storage = createStorage('localStorage'); + // locale + LOCALE = detectLanguage(); - const locale = getLocale(storage); - setLocale(storage, locale); - - // Setting moment - moment.locale(locale); - - // Extract language key. - lang = locale.split('-')[0]; - - // Check if we have a translation in this language. - if (!(lang in translations)) { - lang = defaultLanguage; - } + // moment + moment.locale(LOCALE); + // timeago ta.register('ar', arTA); ta.register('es', esTA); ta.register('da', daTA); ta.register('de', deTA); ta.register('fr', frTA); - ta.register('nl_NL', nlTA); - ta.register('pt_BR', pt_BRTA); - ta.register('zh_CN', zh_CNTA); - ta.register('zh_TW', zh_TWTA); - - timeagoInstance = ta(); + ta.register('nl-NL', nlTA); + ta.register('pt-BR', pt_BRTA); + ta.register('zh-CN', zh_CNTA); + ta.register('zh-TW', zh_TWTA); + TIMEAGO_INSTANCE = ta(); } +/** + * loadTranslations will load the new language pack into the existing ones. + * + * @param {Object} newTranslations translation object to merge into the existing + * languages. + */ export function loadTranslations(newTranslations) { + // Merge the new translations into the existing translations. merge(translations, newTranslations); + + // Push new languages into the supportedLocales array. + Object.keys(newTranslations).forEach(language => { + if (!supportedLocales.includes(language)) { + supportedLocales.push(language); + } + }); } export function timeago(time) { - return timeagoInstance.format(new Date(time), lang); + return TIMEAGO_INSTANCE.format(new Date(time), LOCALE); } /** @@ -140,24 +140,24 @@ export function timeago(time) { */ export function t(key, ...replacements) { let translation; - if (has(translations[lang], key)) { - translation = get(translations[lang], key); + if (has(translations[LOCALE], key)) { + translation = get(translations[LOCALE], key); } else if (has(translations['en'], key)) { translation = get(translations['en'], key); - console.warn(`${lang}.${key} language key not set`); + console.warn(`${LOCALE}.${key} language key not set`); } - if (translation) { - // replace any {n} with the arguments passed to this method - replacements.forEach((str, i) => { - translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str); - }); - - return translation; - } else { - console.warn(`${lang}.${key} and en.${key} language key not set`); + if (!translation) { + console.warn(`${LOCALE}.${key} and en.${key} language key not set`); return key; } + + // Handle replacements in the translation string. + return translation.replace( + /{(\d+)}/g, + (match, number) => + !isUndefined(replacements[number]) ? replacements[number] : match + ); } export default t; diff --git a/client/coral-framework/services/postMessage.js b/client/coral-framework/services/postMessage.js index ef9311e64..6bc18eccf 100644 --- a/client/coral-framework/services/postMessage.js +++ b/client/coral-framework/services/postMessage.js @@ -56,10 +56,10 @@ export function createPostMessage(origin, scope = 'client') { // Send the message. target.postMessage(msg, origin); }, - subscribe: (handler, target = window) => { + subscribe(handler, target = window) { // If this handler is already attached to the target, detach it. if (has(listeners, [target, handler])) { - this.unsubscribeFromMessages(handler, target); + this.unsubscribe(handler, target); } // Wrap the listener with a origin check. @@ -71,7 +71,7 @@ export function createPostMessage(origin, scope = 'client') { // Attach the listener to the target. target.addEventListener('message', listener); }, - unsubscribe: (handler, target = window) => { + unsubscribe(handler, target = window) { if (!has(listeners, [target, handler])) { return; } diff --git a/client/coral-framework/styles/reset.css b/client/coral-framework/styles/reset.css index 8cfd14c41..0a7ddb46b 100644 --- a/client/coral-framework/styles/reset.css +++ b/client/coral-framework/styles/reset.css @@ -6,6 +6,7 @@ border: none; touch-action: manipulation; padding: 0; + margin: 0; overflow: hidden; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index e30c12e06..6351c4484 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -273,3 +273,23 @@ export function translateError(error) { } return error.toString(); } + +/** + * handlePopupAuth will optionally open a popup with the requested uri if the + * window is not already a popup. + * + * @param {String} uri the url to open the window? to + * @param {String} title the title of the new window? to open + * @param {String} features the features to use when opening a window? + */ +export function handlePopupAuth( + uri, + title = 'Login', // TODO: translate + features = 'menubar=0,resizable=0,width=500,height=550,top=200,left=500' +) { + if (window.opener) { + window.location = uri; + } else { + window.open(uri, title, features); + } +} diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index 610557542..e591fa558 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -49,3 +49,18 @@ export const canUsernameBeUpdated = status => { moment(created_at).isAfter(oldestEditTime) ); }; + +/** + * getKarma + * retrieves karma value as string + */ + +export const getKarma = reliability => { + if (reliability === null) { + return 'neutral'; + } else if (reliability) { + return 'good'; + } else { + return 'bad'; + } +}; diff --git a/client/coral-ui/components/BareButton.js b/client/coral-ui/components/BareButton.js index 6d3f9fa93..911d76297 100644 --- a/client/coral-ui/components/BareButton.js +++ b/client/coral-ui/components/BareButton.js @@ -7,17 +7,26 @@ import cn from 'classnames'; * BareButton is a button whose styling is stripped off to a minimum. * Can pass anchor=true to use `a` instead of `button` */ -const BareButton = ({ anchor, className, ...props }) => { - let Element = 'button'; - if (anchor) { - Element = 'a'; +export default class BareButton extends React.Component { + ref = null; + + handleRef = ref => (this.ref = ref); + focus = () => this.ref.focus(); + + render() { + const { anchor, className, ...props } = this.props; + const Element = anchor ? 'a' : 'button'; + return ( + + ); } - return ; -}; +} BareButton.propTypes = { className: PropTypes.string, anchor: PropTypes.bool, }; - -export default BareButton; diff --git a/client/coral-ui/components/Dropdown.css b/client/coral-ui/components/Dropdown.css index c17e6322a..400654f94 100644 --- a/client/coral-ui/components/Dropdown.css +++ b/client/coral-ui/components/Dropdown.css @@ -1,32 +1,27 @@ .dropdown { + display: inline-block; position: relative; - height: 34px; - background: #2c2c2c; - box-sizing: border-box; - color: white; - border-radius: 3px; - box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); - line-height: 20px; - - font-size: 0.98em; - border-radius: 3px; - cursor: pointer; - - &.disabled { - color: #e5e5e5; - background: #888; - cursor: default; - pointer-events: none; - } } .toggle { padding: 8px 45px 8px 15px; outline: none; + color: white; + background: #2c2c2c; + border-radius: 3px; + height: 34px; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); + line-height: 20px; + font-size: 0.98em; &:focus { background: #888; } + + &:disabled { + color: #e5e5e5; + background: #888; + } } .toggleOpen { diff --git a/client/coral-ui/components/Dropdown.js b/client/coral-ui/components/Dropdown.js index bf885477f..380d6c91f 100644 --- a/client/coral-ui/components/Dropdown.js +++ b/client/coral-ui/components/Dropdown.js @@ -4,6 +4,7 @@ import styles from './Dropdown.css'; import Icon from './Icon'; import cn from 'classnames'; import ClickOutside from 'coral-framework/components/ClickOutside'; +import { BareButton } from 'coral-ui'; class Dropdown extends React.Component { toggleRef = null; @@ -88,16 +89,6 @@ class Dropdown extends React.Component { this.toggle(); }; - handleKeyDown = e => { - const code = e.which; - - // 13 = Return, 32 = Space - if (code === 13 || code === 32) { - e.preventDefault(); - this.toggle(); - } - }; - hideMenu = () => { this.setState({ isOpen: false, @@ -155,23 +146,18 @@ class Dropdown extends React.Component { styles.dropdown, className, containerClassName, - 'dd dd-container', - { - [styles.disabled]: disabled, - } + 'dd dd-container' )} > -
      {this.props.icon && (
      + {this.state.isOpen && (
      diff --git a/client/coral-ui/components/Option.css b/client/coral-ui/components/Option.css index 892bc4e7c..cc5ac1263 100644 --- a/client/coral-ui/components/Option.css +++ b/client/coral-ui/components/Option.css @@ -1,7 +1,10 @@ .option { + min-width: 100px; + width: 100%; padding: 10px; outline: none; white-space: nowrap; + text-align: left; &:focus, &:hover { background-color: #ccc; diff --git a/client/coral-ui/components/Option.js b/client/coral-ui/components/Option.js index 704bca179..95473d4c6 100644 --- a/client/coral-ui/components/Option.js +++ b/client/coral-ui/components/Option.js @@ -1,13 +1,15 @@ import React from 'react'; +import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import styles from './Option.css'; import cn from 'classnames'; +import { BareButton } from 'coral-ui'; class Option extends React.Component { ref = null; handleRef = ref => { - this.ref = ref; + this.ref = findDOMNode(ref); }; focus = () => { @@ -19,16 +21,17 @@ class Option extends React.Component { const { className, label = '', onClick, onKeyDown } = this.props; const id = this.props.id ? this.props.id : this.props.value; return ( -
    • - {label} +
    • + + {label} +
    • ); } diff --git a/config.js b/config.js index 6a1f999a7..4ce755f93 100644 --- a/config.js +++ b/config.js @@ -36,6 +36,13 @@ const CONFIG = { // rendered text. DEFAULT_LANG: process.env.TALK_DEFAULT_LANG || 'en', + // WHITELISTED_LANGUAGES is a comma separated list of language/locales that + // should be supported. If the default language is not included in the + // whitelist list, the first entry will be used as the default. + WHITELISTED_LANGUAGES: + process.env.TALK_WHITELISTED_LANGUAGES && + process.env.TALK_WHITELISTED_LANGUAGES.split(',').map(l => l.trim()), + // When TRUE, it ensures that database indexes created in core will not add // indexes. CREATE_MONGO_INDEXES: process.env.DISABLE_CREATE_MONGO_INDEXES !== 'TRUE', @@ -48,6 +55,10 @@ const CONFIG = { // request all of the records. Otherwise, minimum limits of 0 are enforced. ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE', + // ENABLE_STRICT_CSP enables strict CSP enforcement, and will enforce as well + // as report CSP violations. + ENABLE_STRICT_CSP: process.env.TALK_ENABLE_STRICT_CSP === 'TRUE', + // LOGGING_LEVEL specifies the logging level used by the bunyan logger. LOGGING_LEVEL: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes( process.env.TALK_LOGGING_LEVEL @@ -313,6 +324,17 @@ CONFIG.JWT_COOKIE_NAMES = uniq( ]) ); +//------------------------------------------------------------------------------ +// Locale validation +//------------------------------------------------------------------------------ + +if ( + CONFIG.WHITELISTED_LANGUAGES && + !CONFIG.WHITELISTED_LANGUAGES.includes(CONFIG.DEFAULT_LANG) +) { + CONFIG.DEFAULT_LANG = CONFIG.WHITELISTED_LANGUAGES[0]; +} + //------------------------------------------------------------------------------ // External database url's //------------------------------------------------------------------------------ diff --git a/docs/source/02-01-required-configuration.md b/docs/source/02-01-required-configuration.md index be1e1d0b2..a4a26b73d 100644 --- a/docs/source/02-01-required-configuration.md +++ b/docs/source/02-01-required-configuration.md @@ -81,4 +81,4 @@ TALK_JWT_SECRET=jX9y8G2ApcVLwyL{$6s3 Be default, we sign our tokens with HMAC using a SHA-256 hash algorithm. If you want to change the signing algorithm, or use multiple signing/verifying keys, -refer to our [Advanced Configuration](/talk/advanced-configuration/) documentation. +refer to our [Advanced Configuration](/talk/advanced-configuration/#talk-jwt-secret) documentation. diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md index 5b638401d..451865e47 100644 --- a/docs/source/02-02-advanced-configuration.md +++ b/docs/source/02-02-advanced-configuration.md @@ -31,6 +31,21 @@ image you can specify it with `--build-arg TALK_DEFAULT_LANG=en`. Specify the default translation language. (Default `en`) +## TALK_WHITELISTED_LANGUAGES + +This is a **Build Variable** and must be consumed during build. If using the +[Docker-onbuild](/talk/installation-from-docker/#onbuild) +image you can specify it with `--build-arg TALK_WHITELISTED_LANGUAGES=en`. + +Specify the comma separated whitelisted languages that you want the Talk +application to serve. This will override the available set of languages that +Talk will allow to be served. + +If the [TALK_DEFAULT_LANG](#talk-default-lang) is not included in this list of +whitelisted languages, then the first whitelisted language will become the +default language. If this parameter is empty, then all languages supported by +Talk will be whitelisted. (Default '') + ## TALK_DEFAULT_STREAM_TAB This is a **Build Variable** and must be consumed during build. If using the @@ -497,6 +512,15 @@ tracing of GraphQL requests. **Note: Apollo Engine is a premium service, charges may apply.** + + + ## ALLOW_NO_LIMIT_QUERIES Setting this to `TRUE` will allow queries to execute without a limit (returns diff --git a/docs/source/03-07-product-guide-trust.md b/docs/source/03-07-product-guide-trust.md index c27c59dcc..aa0a89259 100644 --- a/docs/source/03-07-product-guide-trust.md +++ b/docs/source/03-07-product-guide-trust.md @@ -3,47 +3,53 @@ title: Trust permalink: /trust/ --- -Trust is a set of components within Talk that incorporate automated moderation -features based on a user's previous behavior. +Trust is a set of components within Talk that incorporate basic automated moderation features based on a user's previous behavior. ## User Karma Score -Using Trust’s calculations, Talk will automatically pre-moderate comments of -users who have a negative karma score. All users start out with a `0` neutral -karma score. If they have a comment approved by a moderator, their score -increases by `1`; if they have a comment rejected by a moderator, it decreases -by `1`. When a commenter is labeled as Unreliable, their comments must be -moderated before they are posted. +Using Trust’s calculations, Talk will automatically hold back, move to the Reported queue, and tag with a 'History' marker, any comments by users who have an Unreliable karma score. (This is for sites who practice post-moderation. If you set pre-moderation of all comments sitewide, this feature has limited use.) -When a commenter has one comment rejected, their next comment must be moderated -once in order to post freely again. If they instead get rejected again, then -they must have two of their comments approved in order to get added back to the -queue. +All users start out with a Neutral karma score (`0`). If they have a comment approved by a moderator, their score increases by `1`; if they have a comment rejected by a moderator, it decreases by `1`. When a commenter's score is labeled as Unreliable, their comments must be approved from the Reported queue before they are posted. Commenters are shown a message stating that a moderator will review their comment shortly. Here are the default thresholds: ```text --2 and lower: Unreliable --1 to +2: Neutral -+3 and higher: Reliable +-1 and lower: Unreliable +0 to +1: Neutral ++2 and higher: Reliable (we don't do anything with this label right now) ``` -You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your -configuration. +So in this case, when a new commenter has their first comment rejected, their user karma score becomes `-1`, which triggers the Unreliable threshhold, and they must then have a comment approved by a moderator in order to post freely again. Until that occurs, all of their comments will be held back temporarily in the Reported queue, marked with a `History` tag. + +If their next comment is also rejected, their user karma score is now `-2`, and they must have two comments approved in order to reach a Neutral score, and post without pre-approval. + +We strongly recommend not telling your community how this system works, or where the threshholds lie. Firstly, they might try to game the system to meet approval, and secondly, it makes it harder for you to change the threshhold in the future. We suggest using language such as "We hold back comments for approval for a variety of reasons, including content, account history, and more." + +If you see that a high proportion of first-time commenters on your site are abusive, you might want to change the threshhold to `0`, at least temporarily. You can configure your own Trust thresholds by using [TRUST_THRESHOLDS](/talk/advanced-configuration/#trust-thresholds) in your site configuration. ## Reliable and Unreliable Flaggers Trust also calculates how reliable users are in terms of the comments they report. This information is displayed to moderators in the User History drawer, -which is accessed by clicking on a user’s name in the Admin. +which is accessed by clicking on a user’s name in the Admin. Currently, no other action is taken based on this score. If a user's reports mostly match what moderators reject, their Report status will display to moderators as Reliable in the user information drawer. If a user's reports mostly differ from what moderators reject, their Report status will show as Unreliable. -If we don't have enough reports to make a call, or the reports even out, their +If Talk doesn't have enough reports to make a call, or the reports even out, their status is Neutral. +Here are the default thresholds: + +```text +-1 and lower: Unreliable +0 to +1: Neutral ++2 and higher: Reliable +``` +You can configure your own Trust thresholds by using [TRUST_THRESHOLDS](/talk/advanced-configuration/#trust-thresholds) in your +configuration. + Note: Report Karma doesn't include reports of "I don't agree with this comment". diff --git a/docs/source/03-08-gdpr.md b/docs/source/03-08-gdpr.md index b6689be0e..e3fd8623f 100644 --- a/docs/source/03-08-gdpr.md +++ b/docs/source/03-08-gdpr.md @@ -11,9 +11,9 @@ can enable the following plugins: - [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association - [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion -Even if you don't reside in a location where GDPR will apply, it is recommended -to enable these features as a best practice to provide your users with control over their -own data. +Even if GDPR will not apply to you, it is recommended to enable these +features as a best practice to provide your users with control over their own +data. ## GPDR Feature Overview diff --git a/docs/source/api/client.md b/docs/source/api/client.md index fd0ec42fc..e5b46a0d4 100644 --- a/docs/source/api/client.md +++ b/docs/source/api/client.md @@ -264,7 +264,7 @@ Coral UI is a set of components to help you build your UI. This powers our core. ### Import ```js -import {Button} 'plugin-api/beta/components/ui'; +import {Button} from 'plugin-api/beta/components/ui'; ``` ### Components diff --git a/docs/source/api/slots.md b/docs/source/api/slots.md index a2e7cc04d..662a025e0 100644 --- a/docs/source/api/slots.md +++ b/docs/source/api/slots.md @@ -99,6 +99,7 @@ You won't have to use this to build plugins, but it's helpful to find where to e * `commentReactions` * `commentActions` * `commentInputArea` +* `commentTombstone` * `draftArea` * `streamSettings` diff --git a/docs/source/integrating/authentication.md b/docs/source/integrating/authentication.md index b7340caf5..77315a9d8 100644 --- a/docs/source/integrating/authentication.md +++ b/docs/source/integrating/authentication.md @@ -25,9 +25,10 @@ state (you don't use the auth anywhere else now). A great example of this is our You can integrate Talk with any authentication service to enable single sign-on for users. The steps to do that are: -1. Create a service that generates [JWT tokens](https://jwt.io). +1. Create a service that generates [JWT tokens](https://jwt.io/introduction/). 2. Push the token into the embed. -3. Implement the `tokenUserNotFound` hook to process the token. +3. Implement the [`tokenUserNotFound`](#implement-tokenusernotfound) hook to + process the token. ### Create JWT Token @@ -39,7 +40,20 @@ Using that demo application, you'll see how you can: 1. Create a node application that can issue JWT's that are compatible with Talk. 2. Provide a validation endpoint that can be used by Talk to validate the token - and get the user via the `tokenUserNotFound` hook. + and get the user via the [`tokenUserNotFound`](#implement-tokenusernotfound) + hook. + +It's also important to note a few requirements for proper integration with Talk. +The generated JWT must contain the following claims: + +- [`jti`](https://tools.ietf.org/html/rfc7519#section-4.1.7): a unique identifier for the token (like a uuid/v4) +- [`exp`](https://tools.ietf.org/html/rfc7519#section-4.1.4): the expiry date of the token as a unix timestamp +- [`sub`](https://tools.ietf.org/html/rfc7519#section-4.1.2): the user identifier that can be used to lookup the user in the mongo + database + - The user may not yet exist in the database, but that's the responsibility + of the [`tokenUserNotFound`](#implement-tokenusernotfound) hook. +- [`iss`](https://tools.ietf.org/html/rfc7519#section-4.1.1): the issuer for the token must match the value of `TALK_JWT_ISSUER` +- [`aud`](https://tools.ietf.org/html/rfc7519#section-4.1.3): the audience for the token must match the value of `TALK_JWT_AUDIENCE` ### Push token into embed @@ -47,7 +61,8 @@ We're assuming that your CMS is capable of authenticating a user account, or at least having the user's details available to send off to the token creation service we created/used in the previous step. -Using the token that was created for the user, you simply have to ammend the template where Talk is rendering to read as the following: +Using the token that was created for the user, you simply have to amend the +template where Talk is rendering to read as the following: ```js Coral.Talk.render(document.getElementById('coralStreamEmbed'), { @@ -72,14 +87,14 @@ example issuer and Talk must match: | Talk | Token Issuer Example | |------|----------------------| -|`JWT_ISSUER`|`JWT_ISSUER`| -|`JWT_AUDIENCE`|`JWT_AUDIENCE`| -|`SECRET`|`JWT_SECRET`*| +|[`TALK_JWT_ISSUER`](/talk/advanced-configuration/#talk-jwt-issuer)|`JWT_ISSUER`| +|[`TALK_JWT_AUDIENCE`](/talk/advanced-configuration/#talk-jwt-audience)|`JWT_AUDIENCE`| +|[`TALK_JWT_SECRET`](/talk/advanced-configuration/#talk-jwt-secret)|`JWT_SECRET`*| \* Note that secrets is a pretty complex topic, refer to the -[TALK-JWT-SECRET](/talk/advanced-configuration/#TALK-JWT-SECRET) configuration +[TALK_JWT_SECRET](/talk/advanced-configuration/#talk-jwt-secret) configuration reference, the basic takeaway is that the secret used to sign the tokens issued by the issuer must be able to be verified by Talk. -For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound) +For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound) reference. diff --git a/docs/themes/coral/source/css/talk.scss b/docs/themes/coral/source/css/talk.scss index afbd6f3af..f3b0c24e8 100644 --- a/docs/themes/coral/source/css/talk.scss +++ b/docs/themes/coral/source/css/talk.scss @@ -291,11 +291,10 @@ pre { .content { article { - p a:not(.plain-link) { - @extend .coral-link; - } + p a:not(.plain-link), ul:not(.toc__menu) li a, ol li a, + td a, dd > a { @extend .coral-link; } diff --git a/errors.js b/errors.js index da1287dd3..39cb6b8ff 100644 --- a/errors.js +++ b/errors.js @@ -161,6 +161,24 @@ class ErrAssetCommentingClosed extends TalkError { } } +// ErrCommentingDisabled is returned when a comment or action is attempted while +// commenting has been disabled site-wide. +class ErrCommentingDisabled extends TalkError { + constructor(message = null) { + super( + 'asset commenting is closed', + { + status: 400, + translation_key: 'COMMENTING_DISABLED', + }, + { + // Include the closedMessage in the metadata piece of the error. + message, + } + ); + } +} + /** * ErrAuthentication is returned when there is an error authenticating and the * message is provided. @@ -387,6 +405,7 @@ module.exports = { ErrAuthentication, ErrCannotIgnoreStaff, ErrCommentTooShort, + ErrCommentingDisabled, ErrContainsProfanity, ErrEditWindowHasEnded, ErrEmailAlreadyVerified, diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index 40305760c..868c8af68 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -71,7 +71,7 @@ const findOrCreateAssetByURL = async (ctx, url) => { // Check for whitelisting + get the settings at the same time. const [whitelisted, settings] = await Promise.all([ DomainList.urlCheck(url), - Settings.load('autoCloseStream closedTimeout'), + Settings.select('autoCloseStream', 'closedTimeout'), ]); // If the domain wasn't whitelisted, then we shouldn't create this asset! diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 37d40a219..b42a636bc 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -94,6 +94,7 @@ const getCommentCountByQuery = (ctx, options) => { author_id, tags, action_type, + excludeDeleted, } = options; // If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs @@ -120,6 +121,12 @@ const getCommentCountByQuery = (ctx, options) => { query.merge({ author_id }); } + if (excludeDeleted) { + // The null query matches documents that either contain the `deleted_at` + // field whose value is null or that do not contain the `deleted_at` field. + query.merge({ deleted_at: null }); + } + if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { query.merge({ [`action_counts.${sc(action_type.toLowerCase())}`]: { @@ -328,11 +335,12 @@ const getCommentsByQuery = async ( sortOrder, sortBy, excludeIgnored, + excludeDeleted, tags, action_type, } ) => { - let comments = CommentModel.find(); + const query = CommentModel.find(); // Enforce that the limit must be gte 0 if this option is not true. if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) { @@ -350,11 +358,17 @@ const getCommentsByQuery = async ( } if (statuses) { - comments = comments.where({ status: { $in: statuses } }); + query.merge({ status: { $in: statuses } }); + } + + if (excludeDeleted) { + // The null query matches documents that either contain the `deleted_at` + // field whose value is null or that do not contain the `deleted_at` field. + query.merge({ deleted_at: null }); } if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { - comments = comments.where({ + query.merge({ [`action_counts.${sc(action_type.toLowerCase())}`]: { $gt: 0, }, @@ -362,7 +376,7 @@ const getCommentsByQuery = async ( } if (ids) { - comments = comments.find({ + query.merge({ id: { $in: ids, }, @@ -370,7 +384,7 @@ const getCommentsByQuery = async ( } if (tags) { - comments = comments.find({ + query.merge({ 'tags.tag.name': { $in: tags, }, @@ -383,17 +397,17 @@ const getCommentsByQuery = async ( (ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) && author_id != null ) { - comments = comments.where({ author_id }); + query.merge({ author_id }); } if (asset_id) { - comments = comments.where({ asset_id }); + query.merge({ asset_id }); } // We perform the undefined check because, null, is a valid state for the // search to be with, which indicates that it is at depth 0. if (parent_id !== undefined) { - comments = comments.where({ parent_id }); + query.merge({ parent_id }); } if ( @@ -402,12 +416,12 @@ const getCommentsByQuery = async ( ctx.user.ignoresUsers && ctx.user.ignoresUsers.length > 0 ) { - comments = comments.where({ + query.merge({ author_id: { $nin: ctx.user.ignoresUsers }, }); } - return executeWithSort(ctx, comments, { cursor, sortOrder, sortBy, limit }); + return executeWithSort(ctx, query, { cursor, sortOrder, sortBy, limit }); }; /** diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index fa0a17459..7010d7533 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,23 +1,72 @@ -const SettingsService = require('../../services/settings'); +const Settings = require('../../services/settings'); const DataLoader = require('dataloader'); +const { zipObject } = require('lodash'); /** - * Creates a set of loaders based on a GraphQL context. - * @param {Object} context the context of the GraphQL request - * @return {Object} object of loaders + * SettingsLoader manages loading specific fields only of the Settings object. */ -module.exports = () => { - const loader = new DataLoader(selections => - Promise.all( - selections.map(fields => { - return SettingsService.retrieve(fields); - }) - ) - ); +class SettingsLoader { + constructor() { + this._loader = new DataLoader(this._batchLoadFn.bind(this)); + this._cache = null; + } - return { - Settings: { - load: (fields = false) => loader.load(fields), - }, - }; -}; + async _batchLoadFn(fields) { + // Load a settings object with all the requested fields, unless we have the + // entire object cached, in which case we'll return the whole cache. + const obj = this._cache + ? await this._cache + : await Settings.select(...fields); + + // Return the specific fields for each of the fields that were loaded. + return fields.map(field => obj[field]); + } + + /** + * load will return the entire Settings object with all fields. + */ + load() { + if (this._cache) { + // Return the cached settings promise. + return this._cache; + } + + // Create a promise that will return the settings object. + const promise = Settings.retrieve(); + + // Set this as the cached value. + this._cache = promise; + + // Return the promised settings. + return promise; + } + + /** + * select will return a promise which resolves to the Settings object that + * contains the requested fields only. + * + * @param {Array} fields the fields from Settings we want to load. + */ + async select(...fields) { + // Load all the values for the specific fields. + const values = await this._loader.loadMany(fields); + + // Zip up the fields and values to create an object to return and return the + // assembled Settings object. + return zipObject(fields, values); + } + + /** + * get, like select, will retrieve the settings, but get will only return a + * single setting. + * + * @param {String} field the field to get + */ + async get(field) { + const value = await this._loader.load(field); + + return value; + } +} + +module.exports = () => ({ Settings: new SettingsLoader() }); diff --git a/graph/mutators/action.js b/graph/mutators/action.js index d759449b0..5557e1a9b 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -14,8 +14,15 @@ const getActionItem = async (ctx, { item_id, item_type }) => { const { loaders: { Comments, Users } } = ctx; switch (item_type) { - case 'COMMENTS': - return Comments.get.load(item_id); + case 'COMMENTS': { + // Get a comment by ID, unless the comment is deleted, then return null. + const comment = await Comments.get.load(item_id); + if (comment.deleted_at) { + return null; + } + + return comment; + } case 'USERS': return Users.getByID.load(item_id); default: diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 1f43ca838..7422f2290 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -70,15 +70,25 @@ const setRole = (ctx, id, role) => { /** * transforms a specific action to a removal action on the target model. */ -const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({ - query: { id: item_id }, - update: { +const actionDecrTransformer = ({ item_id, action_type, group_id }) => { + const update = { $inc: { [`action_counts.${action_type.toLowerCase()}`]: -1, - [`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1, }, - }, -}); + }; + + if (group_id) { + // If the action had a groupID, also decrement that key. + update.$inc[ + `action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}` + ] = -1; + } + + return { + query: { id: item_id }, + update, + }; +}; // delUser will delete a given user with the specified id. const delUser = async (ctx, id) => { @@ -181,10 +191,10 @@ const changeUserPassword = async (ctx, oldPassword, newPassword) => { await Users.changePassword(user.id, newPassword); // Get some context for the email to be sent. - const { organizationName, organizationContactEmail } = await Settings.load([ + const { organizationName, organizationContactEmail } = await Settings.select( 'organizationName', - 'organizationContactEmail', - ]); + 'organizationContactEmail' + ); // Send the password change email. await Users.sendEmail(user, { diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index 6bdf4d19e..3ed5305b7 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -1,4 +1,4 @@ -const { decorateWithTags } = require('./util'); +const { decorateWithTags, getRequestedFields } = require('./util'); const Asset = { async comment({ id }, { id: commentId }, { loaders: { Comments } }) { @@ -64,14 +64,17 @@ const Asset = { return Comments.countByAssetID.load(id); }, - async settings({ settings = null }, _, { loaders: { Settings } }) { + async settings({ settings = null }, _, { loaders: { Settings } }, info) { + // Get the fields we want from the settings. + const fields = getRequestedFields(info); + // Load the global settings, and merge them into the asset specific settings // if we have some. - let globalSettings = await Settings.load(); + let globalSettings = await Settings.select(...fields); if (settings !== null) { - settings = Object.assign({}, globalSettings.toObject(), settings); + settings = Object.assign({}, globalSettings, settings); } else { - settings = globalSettings.toObject(); + settings = globalSettings; } return settings; diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index e562c062c..acd55cb1a 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -57,11 +57,15 @@ const Comment = { asset({ asset_id }, _, { loaders: { Assets } }) { return Assets.getByID.load(asset_id); }, - async editing(comment, _, { loaders: { Settings } }) { - const settings = await Settings.load(); - const editableUntil = new Date( - Number(new Date(comment.created_at)) + settings.editCommentWindowLength + editing: async (comment, _, { loaders: { Settings } }) => { + const editCommentWindowLength = await Settings.get( + 'editCommentWindowLength' ); + + const editableUntil = new Date( + Number(new Date(comment.created_at)) + editCommentWindowLength + ); + return { edited: comment.edited, editableUntil: editableUntil, diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index a8765f743..4bc9d9624 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -15,6 +15,7 @@ const DontAgreeActionSummary = require('./dont_agree_action_summary'); const FlagAction = require('./flag_action'); const FlagActionSummary = require('./flag_action_summary'); const GenericUserError = require('./generic_user_error'); +const KarmaThreshold = require('./karma_threshold'); const LocalUserProfile = require('./local_user_profile'); const RootMutation = require('./root_mutation'); const RootQuery = require('./root_query'); @@ -48,6 +49,7 @@ let resolvers = { FlagAction, FlagActionSummary, GenericUserError, + KarmaThreshold, LocalUserProfile, RootMutation, RootQuery, diff --git a/graph/resolvers/karma_threshold.js b/graph/resolvers/karma_threshold.js new file mode 100644 index 000000000..693b1042c --- /dev/null +++ b/graph/resolvers/karma_threshold.js @@ -0,0 +1,6 @@ +const { property } = require('lodash'); + +module.exports = { + reliable: property('RELIABLE'), + unreliable: property('UNRELIABLE'), +}; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 23fdef288..53296a2ca 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -1,4 +1,4 @@ -const { decorateWithPermissionCheck } = require('./util'); +const { decorateWithPermissionCheck, getRequestedFields } = require('./util'); const { SEARCH_ASSETS, SEARCH_OTHERS_COMMENTS, @@ -16,8 +16,12 @@ const RootQuery = { return Assets.getByURL(query.url); }, - settings(_, args, { loaders: { Settings } }) { - return Settings.load(); + settings(_, args, { loaders: { Settings } }, info) { + // Get the fields we want from the settings. + const fields = getRequestedFields(info); + + // Load only the requested fields. + return Settings.select(...fields); }, // This endpoint is used for loading moderation queues, so hide it in the diff --git a/graph/resolvers/settings.js b/graph/resolvers/settings.js index 6ad1aa455..a299530f2 100644 --- a/graph/resolvers/settings.js +++ b/graph/resolvers/settings.js @@ -1,8 +1,13 @@ const { VIEW_PROTECTED_SETTINGS } = require('../../perms/constants'); - const { decorateWithPermissionCheck } = require('./util'); -const Settings = {}; +const Settings = { + karmaThresholds: ( + settings, + args, + { connectors: { services: { Karma: { THRESHOLDS } } } } + ) => THRESHOLDS, +}; // PROTECTED_SETTINGS are the settings keys that must be protected for only some // eyes. @@ -11,6 +16,7 @@ const PROTECTED_SETTINGS = { autoCloseStream: [VIEW_PROTECTED_SETTINGS], wordlist: [VIEW_PROTECTED_SETTINGS], domains: [VIEW_PROTECTED_SETTINGS], + karmaThresholds: [VIEW_PROTECTED_SETTINGS], }; // decorate the fields on the settings resolver with a permission check. diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index cec2a71dd..a1a745ab2 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -2,7 +2,8 @@ const { ADD_COMMENT_TAG, SEARCH_OTHER_USERS, } = require('../../perms/constants'); -const { property, isBoolean } = require('lodash'); +const { property, isBoolean, pull } = require('lodash'); +const graphqlFields = require('graphql-fields'); /** * getResolver will get the resolver from the typeResolver or apply the default @@ -207,7 +208,11 @@ const decorateWithTags = ( }; }; +const getRequestedFields = info => + pull(Object.keys(graphqlFields(info)), '__typename'); + module.exports = { + getRequestedFields, decorateUserField, decorateWithTags, decorateWithPermissionCheck, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index b5c193e3d..1ca003de6 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -20,9 +20,17 @@ type Reliability { # `null` if the reliability cannot be determined. flagger: Boolean + # flaggerKarma will contains the number of agreed flags vs disagred flag + # count. + flaggerKarma: Int! + # Commenter will be `true` when the commenter is reliable, `false` if not, or # `null` if the reliability cannot be determined. commenter: Boolean + + # commenterKarma the number of approved comments (not untouched) subtracted by + # the number of rejected comments. + commenterKarma: Int! } ################################################################################ @@ -418,6 +426,9 @@ input CommentsQuery { # Exclude comments ignored by the requesting user excludeIgnored: Boolean + + # excludeDeleted when true will exclude deleted comments from the response. + excludeDeleted: Boolean = false } input RepliesQuery { @@ -434,6 +445,9 @@ input RepliesQuery { # Exclude comments ignored by the requesting user excludeIgnored: Boolean + + # excludeDeleted when true will exclude deleted comments from the response. + excludeDeleted: Boolean = false } # CommentCountQuery allows the ability to query comment counts by specific @@ -463,6 +477,9 @@ input CommentCountQuery { # Filter by a specific tag name. tags: [String!] + + # excludeDeleted when true will exclude deleted comments from the count. + excludeDeleted: Boolean = false } # UserCountQuery allows the ability to query user counts by specific @@ -519,7 +536,7 @@ type Comment { replies(query: RepliesQuery = {}): CommentConnection! # replyCount is the number of replies with a depth of 1. Only direct replies - # to this comment are counted. + # to this comment are counted. Deleted comments are included in this count. replyCount: Int # Actions completed on the parent. Requires the `ADMIN` role. @@ -784,6 +801,29 @@ type Domains { whitelist: [String!]! } +# KarmaThreshold defines the bounds for which a User will become unreliable or +# reliable based on their karma score. If the score is equal or less than the +# unreliable value, they are unreliable. If the score is equal or more than the +# reliable value, they are reliable. If they are neither reliable or unreliable +# then they are neutral. +type KarmaThreshold { + reliable: Int! + unreliable: Int! +} + +# KarmaThresholds contains the currently set thresholds for triggering Trust +# beheviour. +type KarmaThresholds { + + # flag represents karma settings in relation to how well a User's flagging + # ability aligns with the moderation decicions made by moderators. + flag: KarmaThreshold! + + # comment represents the karma setting in relation to how well a User's + # comments are moderated. + comment: KarmaThreshold! +} + # Settings stores the global settings for a given installation. type Settings { @@ -828,6 +868,13 @@ type Settings { # closed. closedMessage: String + # disableCommenting will disable commenting site-wide. + disableCommenting: Boolean + + # disableCommentingMessage will be shown above the comment stream while + # commenting is disabled site-wide. + disableCommentingMessage: String + # editCommentWindowLength is the length of time (in milliseconds) after a # comment is posted that it can still be edited by the author. editCommentWindowLength: Int @@ -849,6 +896,10 @@ type Settings { # domains will return a given list of domains. domains: Domains + + # karmaThresholds contains the currently set thresholds for triggering Trust + # beheviour. + karmaThresholds: KarmaThresholds } ################################################################################ @@ -1291,6 +1342,13 @@ input UpdateSettingsInput { # closed. closedMessage: String + # disableCommenting will disable commenting site-wide. + disableCommenting: Boolean + + # disableCommentingMessage will be shown above the comment stream while + # commenting is disabled site-wide. + disableCommentingMessage: String + # charCountEnable is true when the character count restriction is enabled. charCountEnable: Boolean diff --git a/locales/ar.yml b/locales/ar.yml index 852b9ac1b..2b320a621 100644 --- a/locales/ar.yml +++ b/locales/ar.yml @@ -292,55 +292,7 @@ ar: suspect_word: "كلمة مشتبهة" banned_word: "كلمة محظورة" body_count: "يتجاوز النص الحد الأقصى للطول المسموح" - trust: "ثقة" links: "رابط" - modqueue: - account: "account flags" - actions: Actions - all: all - all_streams: "All Streams" - notify_edited: '{0} edited comment "{1}"' - notify_accepted: '{0} accepted comment "{1}"' - notify_rejected: '{0} rejected comment "{1}"' - notify_flagged: '{0} flagged comment "{1}"' - notify_reset: '{0} reset status of comment "{1}"' - approve: "Approve" - approved: "Approved" - ban_user: "Ban" - billion: B - close: Close - empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️" - flagged: flagged - reported: reported - less_detail: "Less detail" - likes: likes - million: M - mod_faster: "Moderate faster with keyboard shortcuts" - moderate: "Moderate →" - more_detail: "More detail" - new: New - newest_first: "Newest First" - navigation: Navigation - next_comment: "Go to the next comment" - toggle_search: "Open search" - next_queue: "Switch queues" - oldest_first: "Oldest First" - premod: pre-mod - prev_comment: "Go to the previous comment" - reject: "Reject" - rejected: "Rejected" - reply: "Reply" - select_stream: "Select Stream" - shift_key: "⇧" - shortcuts: "Shortcuts" - sort: "Sort" - show_shortcuts: "Show Shortcuts" - singleview: "Zen mode" - thismenu: "Open this menu" - jump_to_queue: "Jump to specific queue" - thousand: k - try_these: "Try these" - view_more_shortcuts: "View more shortcuts" my_comment_history: "سجل التعليقات" name: اسم no_agree_comment: "لا أوافق على هذا التعليق" @@ -358,9 +310,6 @@ ar: report_notif: "شكرا على الإبلاغ عن هذا التعليق. تم إبلاغ فريق الإشراف لدينا وسيراجعه قريبًا." report_notif_remove: "لقد تمت إزالة بلاغك." reported: بلغ عنه - comment_history_blank: - title: You have not written any comments - info: A history of your comments will appear here settings: from_settings_page: "من صفحة الملف الشخصي يمكنك مشاهدة سجل التعليقات." my_comment_history: "سجل التعليقات" @@ -378,104 +327,8 @@ ar: step_1_header: "بلغ عن مشكلة" step_2_header: "ساعدنا على الفهم" step_3_header: "شكرا لك على المساهمة الخاصة بك" - streams: - all: All - article: Story - closed: Closed - empty_result: "No assets match this search. Maybe try widening your search?" - filter_streams: "Filter Streams" - newest: Newest - oldest: Oldest - open: Open - pubdate: "Publication Date" - search: Search - sort_by: "Sort By" - status: "Stream Status" - stream_status: "Stream Status" - suspenduser: - title_suspend: "Suspend User" - description_suspend: "You are suspending {0}. This comment will go to the Rejected queue, and {0} will not be allowed to like, report, reply or post until the suspension time is complete." - select_duration: "Select suspension duration" - one_hour: "1 hour" - hours: "{0} hours" - days: "{0} days" - hour: "{0} hours" - day: "{0} days" - cancel: "Cancel" - suspend_user: "Suspend User" - email_message_suspend: "Dear {0},\n\nIn accordance with {1}’s community guidelines, your account has been temporarily suspended. During the suspension, you will be unable to comment, flag or engage with fellow commenters. Please rejoin the conversation {2}." - title_notify: "Notify the user of their temporary suspension" - notify_suspend_until: "User {0} has been temporarily suspended. This suspension will automatically end {1}." - description_notify: "Suspending this user will temporarily disable their account." - write_message: "Write a message" - send: Send - reject_username: - username: username - no_cancel: "No cancel" - description_reject: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily suspend this user until they rewrite their {0}." - title_notify: "Notify the user of their temporary suspension" - description_notify: "Suspending this user will temporarily disable their account." - title_reject: "We noticed you rejected a username" - suspend_user: "Suspend User" - yes_suspend: "Yes suspend" - email_message_reject: "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns." - write_message: "Write a message" - send: Send thank_you: "نحن نقدر سلامتك وردود الفعل. سيراجع المشرف التقرير الخاص بك" - user: - bio_flags: "flags for this bio" - user_bio: "User Bio" - username_flags: "flags for this username" - user_detail: - remove_suspension: "Remove Suspension" - suspend: "Suspend User" - remove_ban: "Remove Ban" - ban: "Ban User" - member_since: "Member Since" - email: "Email" - total_comments: "Total Comments" - reject_rate: "Reject Rate" - reports: "Reports" - all: "All" - rejected: "Rejected" - user_history: "User History" - user_history: - user_banned: "User banned" - ban_removed: "Ban removed" - username_status: "Username {0}" - suspended: "Suspended, {0}" - suspension_removed: "Suspension removed" - system: "System" - date: "Date" - action: "Action" - taken_by: "Taken By" user_impersonating: "هذا المستخدم ينتحل شخصية" user_no_comment: "لم تترك تعليقا مطلقا. إنضم إلى المحادثة!" username_offensive: "اسم المستخدم هذا مسيء" - view_conversation: "عرض المحادثة" - install: - initial: - description: "Let's set up your Talk community in just a few short steps." - submit: "Get Started" - add_organization: - description: "Please tell us the name of your organization. This will appear in emails when inviting new team members." - label: "Organization Name" - save: "Save" - create: - email: "Email address" - username: "Username" - password: "Password" - confirm_password: "Confirm Password" - organization_contact_email: "Organization Contact Email" - save: "Save" - permitted_domains: - title: "Permitted domains" - description: "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)." - submit: "Finish install" - final: - description: "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now." - launch: "Launch Talk" - close: "Close this Installer" - admin_sidebar: - view_options: "View Options" - sort_comments: "Sort Comments" + view_conversation: "عرض المحادثة" \ No newline at end of file diff --git a/locales/da.yml b/locales/da.yml index 4bcb3bb1d..d21a1aee6 100644 --- a/locales/da.yml +++ b/locales/da.yml @@ -211,7 +211,6 @@ da: NO_SPECIAL_CHARACTERS: "Brugernavne kan kun indeholder bogstaver og _" PASSWORD_LENGTH: "Adgangskoden er for kort" PROFANITY_ERROR: "Brugernavne må ikke inholde stødende indhold. Kontakt venligst administratoren, hvis du mener at dette er en fejl." - RATE_LIMIT_EXCEEDED: "Rate limit exceeded" USERNAME_IN_USE: "Brugernavnet er allerede i brug" USERNAME_REQUIRED: "Du skal indtaste et brugernavn" EMAIL_NOT_VERIFIED: "E-mail address not verified" @@ -287,10 +286,8 @@ da: comment_spam: "Spam" comment_noagree: "Uenig" comment_other: "Andre" - suspect_word: "Suspect Word" banned_word: "Forbudt ord" body_count: "Body overstiger max længde" - trust: "Stol" links: "Link" modqueue: account: "konto flag" diff --git a/locales/de.yml b/locales/de.yml index d2b247fbc..2bf137991 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -20,10 +20,13 @@ de: bio_offensive: "Diese Biographie ist unangemessen" cancel: "Abbrechen" confirm_email: + email_confirmation: "E-Mail-Bestätigung" click_to_confirm: "Unten klicken, um E-Mail-Adresse zu bestätigen" confirm: "Bestätigen" password_reset: + mail_sent: 'Falls Sie eine registriertes Konto haben, wurde Ihnen ein Zurücksetzen-Link an diese E-Mail-Adresse geschickt' set_new_password: "Passwort ändern" + change_password_help: "Bitte geben Sie ein neues Passwort ein. Benutzen Sie ein sicheres!" new_password: "Neues Passwort" new_password_help: "Das Passwort benötigt mindestens 8 Zeichen" confirm_new_password: "Neues Passwort bestätigen" @@ -120,9 +123,12 @@ de: custom_css_url: "Benutzerdefinierte CSS-URL" custom_css_url_desc: "URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs" days: Tage - description: "Als Administrator können Sie die Einstellungen für den Kommentarbereich dieses Artikels anpassen:" + description: "Ändern Sie die Einstellungen für den Kommentarbereich dieses Artikels." + disable_commenting_title: "Kommentieren global deaktivieren" + disable_commenting_desc: "Verfassen Sie eine Nachricht, die angezeigt wird, solange das Kommentieren deaktiviert ist." domain_list_text: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll, z.B. für lokale Test- oder Produktionsumgebungen (Bsp.: localhost:3000 staging.domain.com domain.com)." domain_list_title: "Zugelassene Domains" + edit_info: "Information bearbeiten" edit_comment_timeframe_heading: "Zeitlimit zur Bearbeitung von Kommentaren" edit_comment_timeframe_text_pre: "Kommentatoren haben" edit_comment_timeframe_text_post: "Sekunden Zeit, um ihre Kommentare zu bearbeiten." @@ -148,17 +154,31 @@ de: open_stream_configuration: "Dieser Kommentarbereich ist momentan geöffnet. Nach dem Schließen dieses Kommentarbereich wird es nicht mehr möglich sein, zu kommentieren. Bestehende Kommentare bleiben sichtbar." require_email_verification: "E-Mail-Bestätigung erforderlich" require_email_verification_text: "Neue Nutzer müssen ihre E-Mail-Adresse bestätigen." + save: "Speichern" save_changes: "Änderungen speichern" shortcuts: Tastaturkürzel sign_out: "Abmelden" stories: Artikel stream_settings: "Einstellungen Kommentarbereich" + access_message: "Sie müssen Administrator sein, um auf die Einstellungen zuzugreifen. Fragen Sie ggf. einen Administrator, der Ihnen mehr Recht zuweisen kann!" suspect_word_title: "Liste verdächtiger Wörter" suspect_word_text: "Kommentare, die diese Wörter oder Phrasen enthalten (unabhängig von Groß-/Kleinschreibung), werden im Kommentarbereich markiert. Geben Sie ein Wort ein und bestätigen Sie mit Eingabetaste oder Tab. Es ist auch möglich, einen komma-separierten Text einzufügen." tech_settings: "Technische Einstellungen" + organization_information: "Über die Organisation" + organization_info_copy: "Wir verwenden diese Informationen in automatisierten E-Mail-Benachrichtigungen, die Talk versendet. Damit können Nutzer Ihre Organisation identifizieren und sie haben die Möglichkeit bei Problemen in Kontakt mit Ihnen zu treten." + organization_info_copy_2: "Wir empfehlen, einee generische E-Mail-Adresse (z.B. community@yournewsroom.com) für diesen Zweck einzurichten. Die kann über die Zeit gleich bleiben, und gibt nach außen keine Namen preis, die von Nutzern im Fall von Konflikten für persönliche Angriffe missbraucht werden könnten." + organization_details: "Details zur Organisation" + organization_name: "Name der Organisation" + organization_contact_email: "E-Mail-Adresse der Organisation" title: "Kommentarbereich konfigurieren" weeks: Wochen wordlist: "Gesperrte Wörter" + save_changes_dialog: + unsaved_changes: "Ungespeicherte Änderungen" + copy: "Sie haben einen oder mehrere Änderungen vorgenommen, ohne zu speichern. Möchten Sie jetzt speichern oder die Änderungen verwerfen?" + save_settings: "Einstellungen speichern" + discard: "Verwerfen" + cancel: "Abbrechen" continue: "Fortfahren" createdisplay: check_the_form: "Ungültige Eingabe. Bitte prüfen Sie die Felder." @@ -200,11 +220,17 @@ de: we_received_a_request: "Wir haben eine Anfrage erhalten, Ihr Passwort zurückzusetzen. Sollten Sie dies nicht angefordert haben, können Sie diese Nachricht ignorieren." if_you_did: "Falls doch," please_click: "klicken Sie bitte hier zum Zurücksetzen" + subject: "Passwort zurücksetzen" + password_change: + subject: "{0} Passwort-Änderung" + body: "Das Passwort Ihres Benutzerkontos wurde geändert.\n\nFalls Sie diese Änderung nicht angefordert haben, kontaktieren Sie uns bitte unter {0}." embedlink: copy: "In die Zwischenablage kopieren" error: + PASSWORD_INCORRECT: "Ihr bestehendes Passwort wurde falsch eingegeben" COMMENT_PARENT_NOT_VISIBLE: "Der Kommentar, auf den Sie antworten möchten, wurde entfernt oder existiert nicht." EMAIL_VERIFICATION_TOKEN_INVALID: "Code zur E-Mail-Bestätigung ist ungültig." + EMAIL_ALREADY_VERIFIED: "E-Mail-Adresse ist bereits bestätigt." PASSWORD_RESET_TOKEN_INVALID: "Ihr Link zum Passwort zurücksetzen ist ungültig." COMMENT_TOO_SHORT: "Kommentare sollten mehr als ein Zeichen enthalten, bitte überprüfen Sie Ihren Kommentar und probieren Sie es erneut." NOT_AUTHORIZED: "Sie sind nicht berechtigt, diese Aktion auszuführen." @@ -223,19 +249,25 @@ de: LOGIN_MAXIMUM_EXCEEDED: "Sie haben zu häufig erfolglos versucht, sich anzumelden. Bitte warten Sie." PASSWORD_REQUIRED: "Passwort ist erforderlich" COMMENTING_CLOSED: "Kommentarbereich ist bereits geschlossen" + COMMENTING_DISABLED: "Die Kommentarfunktion ist derzeit abgeschaltet" NOT_FOUND: "Ressource nicht gefunden" ALREADY_EXISTS: "Ressource existiert bereits" INVALID_ASSET_URL: "Asset-URL ist ungültig" CANNOT_IGNORE_STAFF: "Mitarbeiter können nicht ignoriert werden." - email: "E-Mail-Adresse ungültig" + INCORRECT_PASSWORD: "Falsches Passwort" + email: "Bitte geben Sie eine gültige E-Mail-Adresse ein." + DELETION_NOT_SCHEDULED: "Löschvorgang wurde nicht geplant" confirm_password: "Passwörter nicht identisch. Bitte erneut überprüfen" network_error: "Server-Verbindung fehlgeschlagen. Bitte überprüfen Sie ihre Internetverbindung und versuchen Sie es erneut." email_not_verified: "E-Mail-Adresse {0} nicht bestätigt." email_password: "E-Mail und/oder Passwort inkorrekt." organization_name: "Namen von Organisationen dürfen nur Buchstaben und Zahlen enthalten." + organization_contact_email: "E-Mail-Adresse der Organisation ist ungültig." password: "Passwort muss mindestens 8 Zeichen enthalten" username: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten" unexpected: "Unerwarteter Fehler aufgetreten. Es tut uns leid!" + required_field: "Dieses Feld ist erforderlich" + temporarily_suspended: "Ihr Konto ist vorübergehend gesperrt. Es wird wieder aktiviert am {0}. Bei Fragen setzen Sie sich mit uns in Kontakt." flag_comment: "Kommentar melden" flag_reason: "Grund der Meldung (optional)" flag_username: "Nutzername melden" @@ -245,6 +277,7 @@ de: comment: Kommentar comment_is_ignored: "Dieser Kommentar ist nicht sichtbar, da Sie den Nutzer ignorieren." comment_is_rejected: "Sie haben diesen Kommentar abgelehnt." + comment_is_deleted: "Der Kommentar wurde vom Nutzer gelöscht." comment_is_hidden: "Dieser Kommentar ist nicht verfügbar." comments: Kommentare configure_stream: "Konfigurieren" @@ -289,7 +322,6 @@ de: suspect_word: "Verdächtiges Wort" banned_word: "Unzulässiges Wort" body_count: "Text überschreitet Zeichenlimit" - trust: "Vertrauen" links: "Link" modqueue: account: "Konto-Markierungen" @@ -333,6 +365,7 @@ de: sort: "Sortieren" show_shortcuts: "Tastaturkürzel anzeigen" singleview: "Zen-Modus" + system_withheld: "System Withheld" thismenu: "Dieses Menü öffnen" jump_to_queue: "Zu bestimmter Liste springen" thousand: T @@ -355,6 +388,9 @@ de: report_notif: "Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern." report_notif_remove: "Ihre Meldung wurde entfernt." reported: Gemeldet + comment_history_blank: + title: Sie haben noch keine Kommentare verfasst + info: Hier wird ein Verlauf Ihrer verfassten Kommentare erscheinen settings: from_settings_page: "Sie können auf Ihrer Profilseite Ihren Kommentarverlauf einsehen." my_comment_history: "Mein Kommentarverlauf" @@ -366,7 +402,7 @@ de: stream: all_comments: "Alle Kommentare" temporarily_suspended: "Entsprechend der Community-Regeln von {0} wurde Ihr Konto vorübergehend gesperrt. Nehmen Sie {1} wieder an der Diskussion teil." - comment_not_found: "Kommentar nicht gefunden" + comment_not_found: "Dieser Kommentar wurde entfernt oder existiert nicht." no_comments: "Es gibt noch keine Kommentare. Schreiben Sie doch einen..." no_comments_and_closed: "Es gab zu diesem Artikel keine Kommentare." step_1_header: "Ein Problem melden" @@ -393,6 +429,8 @@ de: one_hour: "1 Stunde" hours: "{0} Stunden" days: "{0} Tage" + hour: "{0} hours" + day: "{0} days" cancel: "Abbrechen" suspend_user: "Nutzer vorübergehend sperren" email_message_suspend: "Sehr geehrte/r {0}, entsprechend der Community-Richtlinien von {1} wurde Ihr Konto vorübergehend gesperrt. Während der Sperrung können Sie weder kommentieren noch andere Aktionen ausführen. Nehmen Sie {2} wieder an der Diskussion teil." @@ -432,15 +470,15 @@ de: rejected: "Abgelehnte" user_history: "Konto-Verlauf" user_history: - user_banned: "User banned" - ban_removed: "Ban removed" - username_status: "Username {0}" - suspended: "Suspended, {0}" - suspension_removed: "Suspension removed" + user_banned: "Nutzer gesperrt" + ban_removed: "Sperrung aufgehoben" + username_status: "Nutzername {0}" + suspended: "Vorübergehend gesperrt, {0}" + suspension_removed: "Vorübergehende Sperrung aufgehoben" system: "System" - date: "Date" - action: "Action" - taken_by: "Taken By" + date: "Datum" + action: "Aktion" + taken_by: "Durch" user_impersonating: "Gibt sich für jemand anderen aus" user_no_comment: "Sie haben noch keinen Kommentar abgegeben. Teilen Sie Ihre Meinung mit uns!" username_offensive: "Dieser Nutzername ist unangemessen" @@ -458,7 +496,7 @@ de: username: "Nutzername" password: "Passwort" confirm_password: "Passwort bestätigen" - organization_contact_email: "Organization Contact Email" + organization_contact_email: "Kontakt-Adresse der Organisation" save: "Speichern" permitted_domains: title: "Zugelassene Domains" diff --git a/locales/en.yml b/locales/en.yml index e293fb40b..083f0dbe2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -124,6 +124,8 @@ en: custom_css_url_desc: "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external." days: Days description: "Change the comment settings on this story." + disable_commenting_title: "Deactivate commenting site-wide" + disable_commenting_desc: "Write a message that will be displayed while commenting is deactivated." domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)." domain_list_title: "Permitted Domains" edit_info: "Edit Info" @@ -162,7 +164,7 @@ en: suspect_word_title: "Suspect words list" suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list." tech_settings: "Tech Settings" - organization_information: "Organization information" + organization_information: "Organization Information" organization_info_copy: "We use this information in email notifications generated by Talk. This connects the messages to your organization, and provides a way for users to contact you if they have an issue with their account." organization_info_copy_2: "We recommend creating a generic email account (eg. community@yournewsroom.com) for this purpose. This means it can remain consistent over time, and doesn't expose a name that users could target if their account were blocked." organization_details: "Organization Details" @@ -225,6 +227,7 @@ en: embedlink: copy: "Copy to Clipboard" error: + AUTHENTICATION: "An error occurred trying to authenticate your account." PASSWORD_INCORRECT: "Your current password was entered incorrectly" COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist." EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." @@ -247,6 +250,7 @@ en: LOGIN_MAXIMUM_EXCEEDED: "You have made too many unsuccessful password attempts. Please wait." PASSWORD_REQUIRED: "Must input a password" COMMENTING_CLOSED: "Commenting is already closed" + COMMENTING_DISABLED: "Commenting is currently disabled on this site" NOT_FOUND: "Resource not found" ALREADY_EXISTS: "Resource already exists" INVALID_ASSET_URL: "Assert URL is invalid" @@ -274,7 +278,7 @@ en: comment: comment comment_is_ignored: "This comment is hidden because you ignored this user." comment_is_rejected: "You have rejected this comment." - comment_is_deleted: "This comment was deleted." + comment_is_deleted: "This commenter has deleted their account." comment_is_hidden: "This comment is not available." comments: comments configure_stream: "Configure" @@ -319,7 +323,7 @@ en: suspect_word: "Suspect Word" banned_word: "Banned Word" body_count: "Body exceeds max length" - trust: "Trust" + trust: "Karma" links: "Link" modqueue: account: "account flags" @@ -463,10 +467,15 @@ en: email: "Email" total_comments: "Total Comments" reject_rate: "Reject Rate" - reports: "Reports" all: "All" rejected: "Rejected" user_history: "User History" + unreliable: "Unreliable" + karma: "Karma" + learn_more: "Learn More" + user_karma_score: "User Karma Score" + karma_docs_link: "https://docs.coralproject.net/talk/trust/#user-karma-score" + id: "ID" user_history: user_banned: "User banned" ban_removed: "Ban removed" diff --git a/locales/es.yml b/locales/es.yml index ff2777c41..fc9b93b36 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -310,7 +310,6 @@ es: suspect_word: "Palabra sospechosa" banned_word: "Palabra prohibida" body_count: "El texto exede el límite permitido" - trust: "Trust" links: "Link" modqueue: account: "reportes de cuentas" diff --git a/locales/fi_FI.yml b/locales/fi_FI.yml index 71416b4ff..4758a2920 100644 --- a/locales/fi_FI.yml +++ b/locales/fi_FI.yml @@ -292,7 +292,6 @@ fi_FI: suspect_word: "Epäilyttävä sana" banned_word: "Kielletty sana" body_count: "Liian pitkä viesti" - trust: "Luotettava" links: "Linkki" modqueue: account: "Liputuksia" @@ -413,7 +412,7 @@ fi_FI: title_reject: "Huomasimme sinun hylänneen käyttäjänimen" suspend_user: "Aseta väliaikainen käyttökielto" yes_suspend: "Kyllä, sulje väliaikaisesti" - email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää." + email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää." write_message: "Kirjoita viesti" send: Lähetä thank_you: "Arvostamme palautettasi. Moderaattorimme käy läpi tekemäsi ilmiannon." @@ -462,4 +461,4 @@ fi_FI: close: "Sulje asennusnäkymä" admin_sidebar: view_options: "Näytä asetukset" -sort_comments: "Järjestä kommentit" \ No newline at end of file + sort_comments: "Järjestä kommentit" diff --git a/locales/fr.yml b/locales/fr.yml index 1e9e0e496..4e32cf66c 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -300,7 +300,6 @@ fr: suspect_word: "Mot suspect" banned_word: "Mot banni" body_count: "Le texte dépasse la longueur maximale" - trust: "Trust" links: "Lien" modqueue: account: "Signalements du compte" diff --git a/locales/nl_NL.yml b/locales/nl_NL.yml index 9d4057065..297917c9e 100644 --- a/locales/nl_NL.yml +++ b/locales/nl_NL.yml @@ -290,7 +290,6 @@ nl_NL: suspect_word: "Verdacht woord" banned_word: "Geblokeerd woord" body_count: "Tekst is te lang" - trust: "Vertrouwen" links: "Link" modqueue: account: "account meldingen" diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index 4b7b5c3e3..82e2d27d9 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -12,26 +12,26 @@ pt_BR: note_reject_comment: "Banir esse usuário também colocará esse comentário na fila rejeitada." note_ban_user: "Banir este usuário não os permitirá editar comentários ou remover qualquer coisa." yes_ban_user: "Sim, banir o usuário" - write_a_message: "Write a message" - send: "Send" - notify_ban_headline: "Notify the user of ban" - notify_ban_description: "This will notify the user by email that they have been banned from the community" - email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team." + write_a_message: "Escrever uma mensagem" + send: "Enviar" + notify_ban_headline: "Notificar o usuário do banimento" + notify_ban_description: "Isso notificará o usuário por email que ele foi banido da comunidade." + email_message_ban: "Caro {0},\n\nAlguém com acesso a sua conta violou nossas diretrizes da comunidade. Em consequência disso, sua conta foi banida. Você não poderá mais comentar, curtir ou denunciar comentários. Se você acha que isso foi um engano, entre em contato com nossa equipe." bio_offensive: "Esse perfil é ofensiva" cancel: "Cancelar" confirm_email: - click_to_confirm: "Click below to confirm your email address" - confirm: "Confirm" + click_to_confirm: "Clique abaixo para confirmar seu endereço de email" + confirm: "Confirmar" password_reset: - set_new_password: "Change Your Password" - new_password: "New Password" - new_password_help: "Password must be at least 8 characters" - confirm_new_password: "Confirm New Password" - change_password: "Change Password" + set_new_password: "Alterar sua senha" + new_password: "Nova senha" + new_password_help: "A senha deve ter mais de 8 caracteres" + confirm_new_password: "Confirmar nova senha" + change_password: "Alterar senha" characters_remaining: "caracteres restantes" comment: anon: "Anônimo" - undo_reject: "Undo" + undo_reject: "Desfazer" ban_user: "Banir o usuário" comment: "Escreva um comentário..." edited: Editado @@ -61,11 +61,6 @@ pt_BR: reaction: 'Reação' reactions: 'Reações' story: 'Conversas' - flagged_usernames: - notify_approved: '{0} approved username {1}' - notify_rejected: '{0} rejected username {1}' - notify_flagged: '{0} reported username {1}' - notify_changed: 'user {0} changed their username to {1}' community: account_creation_date: "Data de criação da conta" active: Ativo @@ -95,7 +90,7 @@ pt_BR: status: Situação username_and_email: "Nome de usuário e email" yes_ban_user: "Sim, banir este usuário" - commenter: "Commenter" + commenter: "Comentador" configure: apply: Aplicar banned_word_text: "Os comentários que contenham essas palavras ou frases (não sensíveis a maiúsculas e minúsculas) serão automaticamente removidos da lista de comentários. Digite uma palavra e pressione Enter ou Tab para adicionar ou cole uma lista separada por vírgulas." @@ -186,10 +181,10 @@ pt_BR: minutes_plural: "minutos" email: suspended: - subject: "Your account has been suspended" + subject: "Sua conta foi suspensa" banned: - subject: "Your account has been banned" - body: "In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community." + subject: "Sua conta foi banida" + body: "De acordo com as diretrizes da comunidade do Coral Project, sua conta foi banida. Você não tem mais permissão para comentar, sinalizar ou interagir com nossa comunidade." confirm: has_been_requested: "Uma confirmação de e-mail foi solicitada para a seguinte conta:" to_confirm: "Para confirmar a conta, visite este link: " @@ -203,18 +198,18 @@ pt_BR: embedlink: copy: "Copiar para área de transferência" error: - COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist." - EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." - PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid." + COMMENT_PARENT_NOT_VISIBLE: "O comentário que você está respondendo foi removido ou não existe." + EMAIL_VERIFICATION_TOKEN_INVALID: "O token de verificação do email é inválido." + PASSWORD_RESET_TOKEN_INVALID: "Seu link de redefinição de senha é inválido." COMMENT_TOO_SHORT: "Seu comentário precisar ter mais de um caracter. Revise seu comentário e envie novamente" NOT_AUTHORIZED: "Você não está autorizado a executar esta ação." NO_SPECIAL_CHARACTERS: "Nomes de usuários podem conter números de letras e _ somente" PASSWORD_LENGTH: "A senha é muito curta" PROFANITY_ERROR: "Os nomes de usuários não devem conter palavrões. Entre em contato com o administrador se você acredita que isso seja incorreto." - RATE_LIMIT_EXCEEDED: "Rate limit exceeded" + RATE_LIMIT_EXCEEDED: "Tentativas excedidas" USERNAME_IN_USE: "Nome de usuário já em uso" - USERNAME_REQUIRED: "Must input a username" - EMAIL_NOT_VERIFIED: "E-mail address not verified" + USERNAME_REQUIRED: "O nome do usuário é obrigatório" + EMAIL_NOT_VERIFIED: "E-mail não verificado" EDIT_WINDOW_ENDED: "Você não pode mais editar esse comentário. O tempo expirou." EDIT_USERNAME_NOT_AUTHORIZED: "Você não tem permissão para revisar seu nome de usuário." SAME_USERNAME_PROVIDED: "Você deve enviar um nome de usuário diferente." @@ -226,7 +221,7 @@ pt_BR: NOT_FOUND: "Recurso não encontrado" ALREADY_EXISTS: "O recurso já existe" INVALID_ASSET_URL: "O URL do recurso é inválido" - CANNOT_IGNORE_STAFF: "Cannot ignore Staff members." + CANNOT_IGNORE_STAFF: "Não é possível ignorar membros Staff." email: "Não é um e-mail válido" confirm_password: "Suas senhas não coincidem. Por favor, tente novamente." network_error: "Falha ao conectar-se ao servidor. Verifique a sua conexão com a internet e tente novamente." @@ -244,8 +239,8 @@ pt_BR: banned_account_body: "Isso significa que você não pode curtir, informar ou escrever comentários." comment: comentário comment_is_ignored: "Este comentário está oculto porque você ignorou esse usuário." - comment_is_rejected: "You have rejected this comment." - comment_is_hidden: "This comment is not available." + comment_is_rejected: "Você rejeitou este comentário." + comment_is_hidden: "Este comentário não está disponível." comments: comentários configure_stream: "Configurar" content_not_available: "Este conteúdo não está disponível" @@ -255,7 +250,7 @@ pt_BR: label: "Novo usuário" msg: "Sua conta está suspensa porque seu nome de usuário foi considerado inapropriado. Para restaurar sua conta, insira um novo nome de usuário. Entre em contato conosco se você tiver alguma dúvida." changed_name: - msg: "Your username change is under review by our moderation team." + msg: "Sua alteração de nome de usuário está sendo analisada por nossa equipe de moderação." my_comments: "Meus comentários" my_profile: "Meu perfil" new_count: "Ver {0} {1}" @@ -273,24 +268,6 @@ pt_BR: loading_results: "Carregando resultados" marketing: "Isso parece um anúncio/marketing" moderate_this_stream: "Moderar comentários" - flags: - reasons: - user: - username_offensive: "Offensive" - username_nolike: "Dislike" - username_impersonating: "Impersonation" - username_spam: "Spam" - username_other: "Other" - comment: - comment_offensive: "Offensive" - comment_spam: "Spam" - comment_noagree: "Disagree" - comment_other: "Other" - suspect_word: "Suspect Word" - banned_word: "Banned Word" - body_count: "Body exceeds max length" - trust: "Trust" - links: "Link" modqueue: account: "contas marcadas" actions: Ações @@ -302,7 +279,7 @@ pt_BR: notify_flagged: '{0} marcou o comentário "{1}"' approve: "Aprovar" approved: "Aprovado" - ban_user: "Prohibi-lo" + ban_user: "Banir" billion: B close: Fechar dont_like_username: "Eu não gosto desse nome de usuário" @@ -319,22 +296,22 @@ pt_BR: newest_first: "Mais novos primeiro" navigation: Navegação next_comment: "Vá para o próximo comentário" - toggle_search: "Open search" - next_queue: "Switch queues" + toggle_search: "Abrir busca" + next_queue: "Trocar filas" oldest_first: "Mais velhos primeiro" premod: pré-moderação prev_comment: "Vá para o comentário anterior" reject: "Rejeitar" rejected: "Rejeitado" - reply: "Reply" + reply: "Responder" select_stream: "Selecione a lista" shift_key: "⇧" shortcuts: "Atalhos" - sort: "Sort" + sort: "Ordenar" show_shortcuts: "Ver atalhos" singleview: "Modo zen" thismenu: "Abra este menu" - jump_to_queue: "Jump to specific queue" + jump_to_queue: "Ir para fila específica" thousand: k try_these: "Tente este" view_more_shortcuts: "Ver mais atalhos" @@ -343,7 +320,7 @@ pt_BR: no_agree_comment: "Eu não concordo com este comentário" no_like_bio: "Eu não gosto dessa descrição de perfil" no_like_username: "Eu não gosto deste nome de usuário" - already_flagged_username: "You have already flagged this username." + already_flagged_username: "Você já sinalizou este usuário." other: Outro permalink: Compartilhar personal_info: "Este comentário revela informações de identificação pessoal" @@ -367,8 +344,8 @@ pt_BR: all_comments: "Todos os comentários" temporarily_suspended: "De acordo com as diretrizes da comunidade de {0}, sua conta foi temporariamente suspensa. Por favor, volte para a conversa {1}." comment_not_found: "Este comentário foi removido ou não existe." - no_comments: "There are no comments yet, why don’t you write one?" - no_comments_and_closed: "There were no comments on this article." + no_comments: "Ainda não há comentários, por que não escrever um?" + no_comments_and_closed: "Não houve comentários sobre este artigo." step_1_header: "Relatar um problema" step_2_header: "Ajude-nos a entender" step_3_header: "Obrigdo por sua contribuição" @@ -418,29 +395,6 @@ pt_BR: bio_flags: "Marcadas para este perfil" user_bio: "Perfil do usuário" username_flags: "Marcadas para este usuário" - user_detail: - remove_suspension: "Remove Suspension" - suspend: "Suspend User" - remove_ban: "Remove Ban" - ban: "Ban User" - member_since: "Member Since" - email: "Email" - total_comments: "Total Comments" - reject_rate: "Reject Rate" - reports: "Reports" - all: "All" - rejected: "Rejected" - user_history: "User History" - user_history: - user_banned: "User banned" - ban_removed: "Ban removed" - username_status: "Username {0}" - suspended: "Suspended, {0}" - suspension_removed: "Suspension removed" - system: "System" - date: "Date" - action: "Action" - taken_by: "Taken By" user_impersonating: "Este usuário está representando" user_no_comment: "Você nunca deixou um comentário. Participe da conversa!" username_offensive: "Esse nome de usuário é ofensivo" @@ -469,5 +423,5 @@ pt_BR: launch: "Iniciar Talk" close: "Feche este instalador" admin_sidebar: - view_options: "View Options" - sort_comments: "Sort Comments" + view_options: "Opções de visualização" + sort_comments: "Ordenar comentários" \ No newline at end of file diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml index b3307a886..c70f5b779 100644 --- a/locales/zh_CN.yml +++ b/locales/zh_CN.yml @@ -12,22 +12,11 @@ zh_CN: note_reject_comment: "封禁该用户将使这条评论被移入“被拒”队列。" note_ban_user: "封禁该用户将使其无法编辑或删除评论。" yes_ban_user: "是的,封禁该用户" - write_a_message: "Write a message" - send: "Send" - notify_ban_headline: "Notify the user of ban" - notify_ban_description: "This will notify the user by email that they have been banned from the community" - email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team." bio_offensive: "该简介含有冒犯言语" cancel: "取消" confirm_email: click_to_confirm: "Click below to confirm your email address" confirm: "Confirm" - password_reset: - set_new_password: "Change Your Password" - new_password: "New Password" - new_password_help: "Password must be at least 8 characters" - confirm_new_password: "Confirm New Password" - change_password: "Change Password" characters_remaining: "字符剩余可用" comment: anon: "匿名" @@ -61,11 +50,6 @@ zh_CN: reaction: '回应' reactions: '回应' story: '文章' - flagged_usernames: - notify_approved: '{0} approved username {1}' - notify_rejected: '{0} rejected username {1}' - notify_flagged: '{0} reported username {1}' - notify_changed: 'user {0} changed their username to {1}' community: account_creation_date: "账户创建日期" active: "活动中" @@ -203,9 +187,6 @@ zh_CN: embedlink: copy: "复制到粘贴板" error: - COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist." - EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." - PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid." COMMENT_TOO_SHORT: "评论至少应有一个字符。请修改您的评论,再度尝试。" NOT_AUTHORIZED: "您没有权限进行该操作" NO_SPECIAL_CHARACTERS: "用户名只能包含字母、数字跟下划线" @@ -237,7 +218,6 @@ zh_CN: username: "用户名只能包含字母、数字跟下划线" unexpected: "发生了异常错误。对不起!" required_field: "该字段必填" - temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions." flag_comment: "举报评论" flag_reason: "举报理由(可选)" flag_username: "举报用户名" @@ -256,8 +236,6 @@ zh_CN: error: "用户名只能包含字母、数字跟下划线" label: "新用户名" msg: "由于您的用户名不当,您的帐号目前被暂停使用。如要恢复您的帐户,请输入一个新的用户名。如有任何疑问,请与我们联系。" - changed_name: - msg: "Your username change is under review by our moderation team." my_comments: "我的评论" my_profile: "我的资料" new_count: "查看 {0} 更多 {1}" @@ -275,24 +253,6 @@ zh_CN: loading_results: "加载结果中" marketing: "这看起来像是广告" moderate_this_stream: "审查该流" - flags: - reasons: - user: - username_offensive: "Offensive" - username_nolike: "Dislike" - username_impersonating: "Impersonation" - username_spam: "Spam" - username_other: "Other" - comment: - comment_offensive: "Offensive" - comment_spam: "Spam" - comment_noagree: "Disagree" - comment_other: "Other" - suspect_word: "Suspect Word" - banned_word: "Banned Word" - body_count: "Body exceeds max length" - trust: "Trust" - links: "Link" modqueue: account: "帐户标记" actions: "操作" @@ -420,29 +380,6 @@ zh_CN: bio_flags: "对简介的举报" user_bio: "用户简介" username_flags: "对用户名的举报" - user_detail: - remove_suspension: "Remove Suspension" - suspend: "Suspend User" - remove_ban: "Remove Ban" - ban: "Ban User" - member_since: "Member Since" - email: "Email" - total_comments: "Total Comments" - reject_rate: "Reject Rate" - reports: "Reports" - all: "All" - rejected: "Rejected" - user_history: "User History" - user_history: - user_banned: "User banned" - ban_removed: "Ban removed" - username_status: "Username {0}" - suspended: "Suspended, {0}" - suspension_removed: "Suspension removed" - system: "System" - date: "Date" - action: "Action" - taken_by: "Taken By" user_impersonating: "冒名用户" user_no_comment: "您未曾发表评论。现在就来加入对话吧!" username_offensive: "用户名有冒犯性" @@ -468,7 +405,4 @@ zh_CN: final: description: "感谢您安装 Talk!我们已向您的邮箱发送一封验证邮件。当您进行帐号设置时,您可以开始跟您的读者开始互动。" launch: "启动 Talk" - close: "关闭安装程序" - admin_sidebar: - view_options: "View Options" - sort_comments: "Sort Comments" + close: "关闭安装程序" \ No newline at end of file diff --git a/locales/zh_TW.yml b/locales/zh_TW.yml index c186a667f..2b5db5d44 100644 --- a/locales/zh_TW.yml +++ b/locales/zh_TW.yml @@ -12,22 +12,8 @@ zh_TW: note_reject_comment: "封禁該用戶將使這條評論被移入“被拒”列表。" note_ban_user: "封禁該用戶將使其無法編輯或刪除評論。" yes_ban_user: "是的,封禁該用戶" - write_a_message: "Write a message" - send: "Send" - notify_ban_headline: "Notify the user of ban" - notify_ban_description: "This will notify the user by email that they have been banned from the community" - email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team." bio_offensive: "該介紹包含具有攻擊性的內容。" cancel: "取消" - confirm_email: - click_to_confirm: "Click below to confirm your email address" - confirm: "Confirm" - password_reset: - set_new_password: "Change Your Password" - new_password: "New Password" - new_password_help: "Password must be at least 8 characters" - confirm_new_password: "Confirm New Password" - change_password: "Change Password" characters_remaining: "剩餘字符數" comment: anon: "匿名用戶" @@ -61,11 +47,6 @@ zh_TW: reaction: '回應' reactions: '回應' story: '故事' - flagged_usernames: - notify_approved: '{0} approved username {1}' - notify_rejected: '{0} rejected username {1}' - notify_flagged: '{0} reported username {1}' - notify_changed: 'user {0} changed their username to {1}' community: account_creation_date: "賬戶創建日期" active: 激活 @@ -203,9 +184,6 @@ zh_TW: embedlink: copy: "覆制到剪貼板" error: - COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist." - EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." - PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid." COMMENT_TOO_SHORT: "評論長度必須超過一個字符,請您修改評論後重試。" NOT_AUTHORIZED: "您無權執行該操作。" NO_SPECIAL_CHARACTERS: "用戶名只能包含字母、數字和下劃線" @@ -237,7 +215,6 @@ zh_TW: username: "用戶名只能包含字母、數字和下劃線。" unexpected: "發生了意外錯誤。抱歉!" required_field: "該字段必填" - temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions." flag_comment: "舉報評論" flag_reason: "舉報原因(可選)" flag_username: "舉報用戶名" @@ -256,8 +233,6 @@ zh_TW: error: "用戶名只能包含字母、數字和下劃線。" label: "新用戶名" msg: "由於您的用戶名不當,您的帳號目前已被暫停使用。如要恢復您的帳戶,請輸入一個新的用戶名。如有任何疑問,請與我們聯繫。" - changed_name: - msg: "Your username change is under review by our moderation team." my_comments: "我的評論" my_profile: "我的概況" new_count: "查看{0}更多{1}" @@ -275,24 +250,6 @@ zh_TW: loading_results: "加載結果" marketing: "這看起來像是廣告/營銷" moderate_this_stream: "審核這個流" - flags: - reasons: - user: - username_offensive: "Offensive" - username_nolike: "Dislike" - username_impersonating: "Impersonation" - username_spam: "Spam" - username_other: "Other" - comment: - comment_offensive: "Offensive" - comment_spam: "Spam" - comment_noagree: "Disagree" - comment_other: "Other" - suspect_word: "Suspect Word" - banned_word: "Banned Word" - body_count: "Body exceeds max length" - trust: "Trust" - links: "Link" modqueue: account: "帳戶標記" actions: 操作 @@ -345,7 +302,6 @@ zh_TW: no_agree_comment: "我不同意這個評論" no_like_bio: "我不喜歡這個個人簡介" no_like_username: "我不喜歡這個用戶名" - already_flagged_username: "You have already flagged this username." other: 其他 permalink: 分享 personal_info: "該評論洩露了個人身份資訊" @@ -420,29 +376,6 @@ zh_TW: bio_flags: "該簡介的標記" user_bio: "用戶簡介" username_flags: "該用戶名的標記" - user_detail: - remove_suspension: "Remove Suspension" - suspend: "Suspend User" - remove_ban: "Remove Ban" - ban: "Ban User" - member_since: "Member Since" - email: "Email" - total_comments: "Total Comments" - reject_rate: "Reject Rate" - reports: "Reports" - all: "All" - rejected: "Rejected" - user_history: "User History" - user_history: - user_banned: "User banned" - ban_removed: "Ban removed" - username_status: "Username {0}" - suspended: "Suspended, {0}" - suspension_removed: "Suspension removed" - system: "System" - date: "Date" - action: "Action" - taken_by: "Taken By" user_impersonating: "此用戶正在冒充" user_no_comment: "您尚未評論過。加入對話吧!" username_offensive: "這個用戶名有冒犯性" @@ -469,6 +402,3 @@ zh_TW: description: "感謝安裝Talk!我們給您發送了一封郵件以驗證您的電子郵箱地址。在完成帳戶設置後,您即可開始與您的讀者互動。" launch: "啟動Talk" close: "關閉安裝程序" - admin_sidebar: - view_options: "View Options" - sort_comments: "Sort Comments" diff --git a/middleware/contentSecurityPolicy.js b/middleware/contentSecurityPolicy.js new file mode 100644 index 000000000..c47faa74e --- /dev/null +++ b/middleware/contentSecurityPolicy.js @@ -0,0 +1,55 @@ +const helmet = require('helmet'); +const { WEBSOCKET_LIVE_URI, ENABLE_STRICT_CSP } = require('../config'); +const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url'); +const { URL } = require('url'); + +// websocketUri represents the host where we can connect for websocket requests. +const websocketUri = new URL(WEBSOCKET_LIVE_URI || BASE_URL); +websocketUri.protocol = websocketUri.protocol.startsWith('https') + ? 'wss' + : 'ws'; +const { origin: websocketSrc } = websocketUri; + +// staticSrc represents any static asset hosted on the static host. +const { host: staticSrc } = new URL(STATIC_URL); + +// nonceSrc represents the nonce source that is used to indicate a safe resource +// to load. +const nonceSrc = (req, res) => `'nonce-${res.locals.nonce}'`; + +module.exports = helmet.contentSecurityPolicy({ + directives: { + reportUri: `${BASE_PATH}api/v1/csp`, // report all policy violations to our reporting uri + defaultSrc: ["'none'"], // by default, do not allow anything at all + scriptSrc: [ + "'self'", + 'https://ajax.googleapis.com', // for jquery + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + styleSrc: [ + "'self'", + 'https://maxcdn.bootstrapcdn.com', // for bootstrap css + 'https://fonts.googleapis.com', // for google fonts + 'https://code.getmdl.io', // for mdl css + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + connectSrc: ["'self'", websocketSrc], + fontSrc: [ + "'self'", + 'https://maxcdn.bootstrapcdn.com', // for font-awesome + 'https://fonts.gstatic.com', // for google fonts + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + imgSrc: [ + "'self'", + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + }, + browserSniff: false, + // Allow the configuration to disable strict enforcement of CSP. + reportOnly: !ENABLE_STRICT_CSP, +}); diff --git a/middleware/nonce.js b/middleware/nonce.js new file mode 100644 index 000000000..7d072cf37 --- /dev/null +++ b/middleware/nonce.js @@ -0,0 +1,16 @@ +const { get, merge } = require('lodash'); +const uuid = require('uuid/v4'); + +// nonce is designed to create a random value that can be used in conjunction +// with the csp middleware. +module.exports = (req, res, next) => { + const nonce = uuid(); + + // Attach the nonce to the locals. + res.locals.nonce = nonce; + res.locals.data = merge({}, get(res.locals, 'data', {}), { + SCRIPT_NONCE: nonce, + }); + + next(); +}; diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js index 0dc05a7be..8d467fcda 100644 --- a/middleware/staticTemplate.js +++ b/middleware/staticTemplate.js @@ -48,7 +48,7 @@ const attachStaticLocals = locals => { for (const key in TEMPLATE_LOCALS) { const value = TEMPLATE_LOCALS[key]; - locals[key] = value; + merge(locals, { [key]: value }); } }; @@ -94,9 +94,13 @@ const createResolveFactory = (() => { module.exports = async (req, res, next) => { try { - // Attach the custom css url. - const { customCssUrl } = await SettingsService.retrieve('customCssUrl'); + // Attach the custom css url and organization name. + const { customCssUrl, organizationName } = await SettingsService.select( + 'customCssUrl', + 'organizationName' + ); res.locals.customCssUrl = customCssUrl; + res.locals.organizationName = organizationName; } catch (err) { console.warn(err); } diff --git a/models/schema/action.js b/models/schema/action.js index 1df7bd793..d2047faac 100644 --- a/models/schema/action.js +++ b/models/schema/action.js @@ -9,7 +9,8 @@ const Action = new Schema( id: { type: String, default: uuid.v4, - unique: true, + unique: 1, + index: 1, }, action_type: { type: String, @@ -19,7 +20,10 @@ const Action = new Schema( type: String, enum: ITEM_TYPES, }, - item_id: String, + item_id: { + type: String, + index: 1, + }, user_id: String, // The element that summaries will additionally group on in addtion to their action_type, item_type, and @@ -37,15 +41,4 @@ const Action = new Schema( } ); -// Create an index on the `item_id` field so that queries looking for -// actions based on the item id can resolve faster. -Action.index( - { - item_id: 1, - }, - { - background: true, - } -); - module.exports = Action; diff --git a/models/schema/asset.js b/models/schema/asset.js index 043bc9a3f..82de92120 100644 --- a/models/schema/asset.js +++ b/models/schema/asset.js @@ -43,8 +43,7 @@ const Asset = new Schema( modified_date: Date, // This object is used exclusively for storing settings that are to override - // the base settings from the base Settings object. This is to be accessed - // always after running `rectifySettings` against it. + // the base settings from the base Settings object. settings: { default: {}, type: Object, diff --git a/models/schema/comment.js b/models/schema/comment.js index a211884ff..7ae51ff41 100644 --- a/models/schema/comment.js +++ b/models/schema/comment.js @@ -55,12 +55,16 @@ const Comment = new Schema( type: String, default: uuid.v4, unique: true, + index: true, }, body: { type: String, }, body_history: [BodyHistoryItemSchema], - asset_id: String, + asset_id: { + type: String, + index: true, + }, author_id: String, status_history: [Status], status: { @@ -90,7 +94,6 @@ const Comment = new Schema( // deleted_at stores the date that the given comment was deleted. deleted_at: { type: Date, - default: null, }, // Additional metadata stored on the field. @@ -110,95 +113,67 @@ const Comment = new Schema( } ); -// Add the indexes for the id of the comment. Comment.index( { - id: 1, - }, - { - unique: true, - background: false, - } -); - -Comment.index( - { - status: 1, - created_at: 1, - }, - { - background: true, - } -); - -Comment.index( - { - status: 1, - created_at: 1, - asset_id: 1, - }, - { - background: true, - } -); - -// Create a sparse index to search across. -Comment.index( - { - created_at: 1, - 'action_counts.flag': 1, - status: 1, - }, - { - background: true, - sparse: true, - } -); - -// Create a sparse index to search across. -Comment.index( - { - 'action_counts.flag': 1, - status: 1, - }, - { - background: true, - sparse: true, - } -); - -// Add an index that is optimized for finding flagged comments. -Comment.index( - { - asset_id: 1, - created_at: 1, - 'action_counts.flag': 1, - }, - { - background: true, - } -); - -// Add an index for the reply sort. -Comment.index( - { - asset_id: 1, + deleted_at: 1, created_at: -1, - reply_count: -1, }, - { - background: true, - } + { partialFilterExpression: { deleted_at: null } } ); -// Add an index that is optimized for finding a user's comments. Comment.index( { - author_id: 1, + deleted_at: 1, + status: 1, + created_at: -1, + }, + { partialFilterExpression: { deleted_at: null } } +); + +Comment.index({ + asset_id: 1, + created_at: -1, +}); + +Comment.index({ + asset_id: 1, + created_at: 1, +}); + +Comment.index({ + author_id: 1, + created_at: -1, +}); + +Comment.index({ + asset_id: 1, + status: 1, +}); + +Comment.index({ + asset_id: 1, + parent_id: 1, + reply_count: -1, + created_at: -1, +}); + +Comment.index({ + asset_id: 1, + reply_count: -1, + created_at: -1, +}); + +Comment.index( + { + 'action_counts.flag': 1, + status: 1, created_at: -1, }, { - background: true, + partialFilterExpression: { + 'action_counts.flag': { $exists: true, $gt: 0 }, + deleted_at: null, + }, } ); @@ -210,34 +185,10 @@ Comment.index( status: 1, }, { - background: true, - } -); - -// Optimize for tag searches/counts. -Comment.index( - { - 'tags.tag.name': 1, - status: 1, - }, - { - background: true, sparse: true, } ); -// Add an index that is optimized for sorting based on the created_at timestamp -// but also good at locating comments that have a specific asset id. -Comment.index( - { - asset_id: 1, - created_at: 1, - }, - { - background: true, - } -); - Comment.virtual('edited').get(function() { return this.body_history.length > 1; }); diff --git a/models/schema/setting.js b/models/schema/setting.js index af7932910..d68ee7acd 100644 --- a/models/schema/setting.js +++ b/models/schema/setting.js @@ -12,6 +12,8 @@ const Setting = new Schema( id: { type: String, default: '1', + unique: 1, + index: true, }, moderation: { type: String, @@ -66,6 +68,14 @@ const Setting = new Schema( type: String, default: 'Expired', }, + disableCommenting: { + type: Boolean, + default: false, + }, + disableCommentingMessage: { + type: String, + default: '', + }, wordlist: { banned: { type: Array, @@ -125,21 +135,4 @@ const Setting = new Schema( } ); -/** - * Merges two settings objects. - */ -Setting.method('merge', function(src) { - Setting.eachPath(path => { - // Exclude internal fields... - if (['id', '_id', '__v', 'created_at', 'updated_at'].includes(path)) { - return; - } - - // If the source object contains the path, shallow copy it. - if (path in src) { - this[path] = src[path]; - } - }); -}); - module.exports = Setting; diff --git a/models/schema/user.js b/models/schema/user.js index ec9c018cc..a27b8dc39 100644 --- a/models/schema/user.js +++ b/models/schema/user.js @@ -58,6 +58,7 @@ const User = new Schema( default: uuid.v4, unique: true, required: true, + index: true, }, // This is sourced from the social provider or set manually during user setup @@ -107,6 +108,7 @@ const User = new Schema( status: { type: String, enum: USER_STATUS_USERNAME, + index: true, }, // History stores the history of username status changes. @@ -135,6 +137,7 @@ const User = new Schema( type: Boolean, required: true, default: false, + index: true, }, history: [ { @@ -226,41 +229,26 @@ User.index( } ); -User.index( - { - lowercaseUsername: 1, - 'profiles.id': 1, - created_at: -1, - }, - { - background: true, - } -); +User.index({ + lowercaseUsername: 1, + 'profiles.id': 1, + created_at: -1, +}); // This query is executed often, to count the number of flagged accounts with // usernames. -User.index( - { - 'action_counts.flag': 1, - 'status.username.status': 1, - }, - { - background: true, - } -); +User.index({ + 'action_counts.flag': 1, + 'status.username.status': 1, +}); // Sorting users by created at is the default people search. -User.index( - { - created_at: -1, - }, - { - background: true, - } -); +User.index({ + created_at: -1, +}); /** - * returns true if a commenter is staff + * returns true if a commenter is staff. */ User.method('isStaff', function() { return this.role !== 'COMMENTER'; @@ -330,6 +318,9 @@ User.virtual('hasVerifiedEmail').get(function() { }); }); +/** + * system returns true when the user is a system user. + */ User.virtual('system') .get(function() { return this._system; @@ -348,6 +339,11 @@ User.virtual('banned') }) .set(function(status) { this.status.banned.status = status; + + if (!this.status.banned.history) { + this.status.banned.history = []; + } + this.status.banned.history.push({ status, created_at: new Date(), @@ -366,6 +362,11 @@ User.virtual('suspended') }) .set(function(until) { this.status.suspension.until = until; + + if (!this.status.suspension.history) { + this.status.suspension.history = []; + } + this.status.suspension.history.push({ until, created_at: new Date(), diff --git a/nightwatch.conf.js b/nightwatch.conf.js index 75dfa5ef6..16777b844 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -10,15 +10,15 @@ module.exports = { selenium: { start_process: true, server_path: - 'node_modules/selenium-standalone/.selenium/selenium-server/3.7.1-server.jar', + 'node_modules/selenium-standalone/.selenium/selenium-server/3.8.1-server.jar', log_path: REPORTS_FOLDER, host: '127.0.0.1', port: 6666, cli_args: { 'webdriver.chrome.driver': - 'node_modules/selenium-standalone/.selenium/chromedriver/2.33-x64-chromedriver', + 'node_modules/selenium-standalone/.selenium/chromedriver/2.37-x64-chromedriver', 'webdriver.gecko.driver': - 'node_modules/selenium-standalone/.selenium/geckodriver/0.19.1-x64-geckodriver', + 'node_modules/selenium-standalone/.selenium/geckodriver/0.20.1-x64-geckodriver', }, }, test_settings: { diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d6105c594..000000000 --- a/package-lock.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "talk", - "version": "4.3.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "exenv": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" - }, - "react-side-effect": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.5.tgz", - "integrity": "sha512-Z2ZJE4p/jIfvUpiUMRydEVpQRf2f8GMHczT6qLcARmX7QRb28JDBTpnM2g/i5y/p7ZDEXYGHWg0RbhikE+hJRw==", - "requires": { - "exenv": "1.2.2", - "shallowequal": "1.0.2" - } - }, - "shallowequal": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz", - "integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw==" - } - } -} diff --git a/package.json b/package.json index 9ec249f83..a313c1ec0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "4.4.0", + "version": "4.4.2", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "private": true, @@ -58,7 +58,6 @@ "dependencies": { "@coralproject/gql-merge": "^0.1.0", "@coralproject/graphql-anywhere-optimized": "^0.1.0", - "accepts": "^1.3.4", "apollo-client": "^1.9.1", "apollo-engine": "^0.8.1", "apollo-server-express": "^1.2.0", @@ -84,13 +83,14 @@ "brotli-webpack-plugin": "^0.5.0", "bunyan": "^1.8.12", "bunyan-debug-stream": "^1.0.8", - "cli-table": "^0.3.1", + "cli-table2": "^0.2.0", "clipboard": "^1.7.1", "colors": "^1.1.2", "commander": "^2.11.0", "common-tags": "^1.4.0", "compression": "1.7.1", "compression-webpack-plugin": "^1.0.0", + "consolidate": "0.14.0", "cookie-parser": "^1.4.3", "copy-webpack-plugin": "^4.0.0", "cross-spawn": "^5.1.0", @@ -108,12 +108,14 @@ "express-static-gzip": "^0.3.1", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^0.11.2", + "fluent-langneg": "^0.1.0", "form-data": "^2.3.1", "fs-extra": "^4.0.1", "graphql": "^0.10.1", "graphql-ast-tools": "0.2.3", "graphql-docs": "0.2.0", "graphql-errors": "^2.1.0", + "graphql-fields": "^1.0.2", "graphql-redis-subscriptions": "1.3.0", "graphql-subscriptions": "^0.4.3", "graphql-tag": "^1.2.3", @@ -148,7 +150,7 @@ "metascraper-title": "^3.9.2", "minimist": "^1.2.0", "moment": "^2.18.1", - "mongoose": "^4.12.3", + "mongoose": "^5.1.1", "ms": "^2.0.0", "murmurhash-js": "^1.0.0", "name-all-modules-plugin": "^1.0.1", @@ -156,6 +158,7 @@ "node-fetch": "^1.7.2", "nodemailer": "^2.6.4", "npm-run-all": "^4.1.2", + "nunjucks": "^3.1.3", "parallel-webpack": "^2.2.0", "passport": "^0.4.0", "passport-jwt": "^3.0.0", @@ -218,6 +221,7 @@ "babel-plugin-dynamic-import-node": "^1.1.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", "browserstack-local": "^1.3.0", + "casual": "^1.5.19", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-datetime": "^1.5.0", @@ -229,7 +233,7 @@ "husky": "^0.14.3", "identity-obj-proxy": "^3.0.0", "jest-junit": "^3.6.0", - "lint-staged": "^7.0.0", + "lint-staged": "^7.1.0", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", "nightwatch": "^0.9.16", diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index aeb9841f9..0d1893066 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -9,4 +9,5 @@ export { getDefinitionName, getShallowChanges, createDefaultResponseFragments, + handlePopupAuth, } from 'coral-framework/utils'; diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index 5aa3063b3..f1f25b261 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -11,10 +11,22 @@ function getReactionConfig(reaction) { if (CREATE_MONGO_INDEXES) { // Create the index on the comment model based on the reaction config. - Comment.collection.createIndex( + Comment.collection.ensureIndex( { - created_at: 1, - [`action_counts.${sc(reaction)}`]: 1, + asset_id: 1, + [`action_counts.${sc(reaction)}`]: -1, + created_at: -1, + }, + { + background: true, + } + ); + + Comment.collection.ensureIndex( + { + asset_id: 1, + [`action_counts.${sc(reaction)}`]: -1, + created_at: -1, }, { background: true, diff --git a/plugins/talk-plugin-akismet/server/hooks.js b/plugins/talk-plugin-akismet/server/hooks.js index 80a233584..4e24af4b3 100644 --- a/plugins/talk-plugin-akismet/server/hooks.js +++ b/plugins/talk-plugin-akismet/server/hooks.js @@ -28,8 +28,11 @@ let enabled = true; module.exports = { RootMutation: { createComment: { - async pre(_, { input }, { loaders, parent: req }) { - // If the key validation failed, then we can't run with the client. + async pre(_, { input }, ctx) { + const req = ctx.parent.parent; + const loaders = ctx.loaders; + + //If the key validation failed, then we can't run with the client. if (!enabled) { debug('not enabled, passing'); return; @@ -71,7 +74,7 @@ module.exports = { permalink: asset.url, comment_type: 'comment', comment_content: input.body, - is_test: true, + is_test: false, }); debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`); diff --git a/plugins/talk-plugin-auth/client/login/components/SignUp.js b/plugins/talk-plugin-auth/client/login/components/SignUp.js index 96dadcd67..878b53b5d 100644 --- a/plugins/talk-plugin-auth/client/login/components/SignUp.js +++ b/plugins/talk-plugin-auth/client/login/components/SignUp.js @@ -75,7 +75,7 @@ class SignUp extends React.Component { showErrors={!!emailError} errorMsg={emailError} onChange={this.handleEmailChange} - autocomplete="off" + autoComplete="off" /> {passwordError && ( @@ -116,7 +116,7 @@ class SignUp extends React.Component { errorMsg={passwordRepeatError} onChange={this.handlePasswordRepeatChange} minLength="8" - autocomplete="off" + autoComplete="off" /> (dispatch, _, { rest }) => { - window.location = `${rest.uri}/auth/facebook`; + handlePopupAuth(`${rest.uri}/auth/facebook`); }; diff --git a/plugins/talk-plugin-facebook-auth/client/index.js b/plugins/talk-plugin-facebook-auth/client/index.js index cb8a8f059..cf2ac32ce 100644 --- a/plugins/talk-plugin-facebook-auth/client/index.js +++ b/plugins/talk-plugin-facebook-auth/client/index.js @@ -5,6 +5,7 @@ import translations from './translations.yml'; export default { translations, slots: { + authExternalAdminSignIn: [SignIn], authExternalSignIn: [SignIn], authExternalSignUp: [SignUp], }, diff --git a/plugins/talk-plugin-google-auth/client/actions.js b/plugins/talk-plugin-google-auth/client/actions.js index 8b49bf39e..1856ddb54 100644 --- a/plugins/talk-plugin-google-auth/client/actions.js +++ b/plugins/talk-plugin-google-auth/client/actions.js @@ -1,3 +1,5 @@ +import { handlePopupAuth } from 'plugin-api/beta/client/utils'; + export const loginWithGoogle = () => (dispatch, _, { rest }) => { - window.location = `${rest.uri}/auth/google`; + handlePopupAuth(`${rest.uri}/auth/google`); }; diff --git a/plugins/talk-plugin-google-auth/client/index.js b/plugins/talk-plugin-google-auth/client/index.js index cb8a8f059..cf2ac32ce 100644 --- a/plugins/talk-plugin-google-auth/client/index.js +++ b/plugins/talk-plugin-google-auth/client/index.js @@ -5,6 +5,7 @@ import translations from './translations.yml'; export default { translations, slots: { + authExternalAdminSignIn: [SignIn], authExternalSignIn: [SignIn], authExternalSignUp: [SignUp], }, diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 5aa834034..efd4cd366 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -10,16 +10,22 @@ import { bindActionCreators } from 'redux'; import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions'; import { notify } from 'plugin-api/beta/client/actions/notification'; import { t } from 'plugin-api/beta/client/services'; +import { getErrorMessages } from 'coral-framework/utils'; class IgnoreUserConfirmationContainer extends React.Component { - ignoreUser = () => { + ignoreUser = async () => { const { ignoreUser, notify, comment, closeMenu } = this.props; - ignoreUser(comment.user.id).then(() => { + + try { + await ignoreUser(comment.user.id); notify( 'success', t('talk-plugin-ignore-user.notify_success', comment.user.username) ); - }); + } catch (err) { + notify('error', getErrorMessages(err)); + } + closeMenu(); }; diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml index 84a4ca11c..fdf2b1daf 100644 --- a/plugins/talk-plugin-ignore-user/client/translations.yml +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -39,6 +39,7 @@ en: confirmation_title: Ignore {0}? de: talk-plugin-ignore-user: + blank_info: Sie ignorieren derzeit keine Nutzer section_title: Ignorierte Nutzer section_info: Weil Sie die folgenden Nutzer ignorieren, sind deren Kommentare versteckt. stop_ignoring: Ignorieren beenden diff --git a/plugins/talk-plugin-local-auth/client/actions.js b/plugins/talk-plugin-local-auth/client/actions.js new file mode 100644 index 000000000..972e1f6fd --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/actions.js @@ -0,0 +1,9 @@ +import * as actions from './constants'; + +export const startAttach = () => ({ + type: actions.START_ATTACH, +}); + +export const finishAttach = () => ({ + type: actions.FINISH_ATTACH, +}); diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js index dc7e0d974..a5bfd7096 100644 --- a/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js +++ b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js @@ -45,6 +45,10 @@ class AddEmailAddressDialog extends React.Component { ), }; + componentDidMount() { + this.props.startAttach(); + } + onChange = e => { const { name, value } = e.target; this.setState( @@ -99,7 +103,13 @@ class AddEmailAddressDialog extends React.Component { }); }; - confirmChanges = async () => { + finish = () => { + this.props.finishAttach(); + }; + + confirmChanges = async e => { + e.preventDefault(); + if (!this.validate()) { this.showErrors(); return; @@ -113,7 +123,11 @@ class AddEmailAddressDialog extends React.Component { email: emailAddress, password: confirmPassword, }); - this.props.notify('success', 'Email Added!'); + + this.props.notify( + 'success', + t('talk-plugin-local-auth.add_email.added.alert') + ); this.goToNextStep(); } catch (err) { this.props.notify('error', getErrorMessages(err)); @@ -143,13 +157,13 @@ class AddEmailAddressDialog extends React.Component { )} {step === 1 && !settings.requireEmailConfirmation && ( - {}} /> + )} {step === 1 && settings.requireEmailConfirmation && ( {}} + done={this.finish} /> )} @@ -161,6 +175,8 @@ AddEmailAddressDialog.propTypes = { attachLocalAuth: PropTypes.func.isRequired, notify: PropTypes.func.isRequired, root: PropTypes.object.isRequired, + startAttach: PropTypes.func.isRequired, + finishAttach: PropTypes.func.isRequired, }; export default AddEmailAddressDialog; diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js b/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js index 8d3fc308f..de296c812 100644 --- a/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js +++ b/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js @@ -41,7 +41,7 @@ const AddEmailContent = ({
    -
    +
    diff --git a/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js b/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js index f29dd8d86..f671c065b 100644 --- a/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js +++ b/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js @@ -4,10 +4,74 @@ import styles from './ChangeEmailContentDialog.css'; import InputField from './InputField'; import { Button } from 'plugin-api/beta/client/components/ui'; import { t } from 'plugin-api/beta/client/services'; +import validate from 'coral-framework/helpers/validate'; +import errorMsj from 'coral-framework/helpers/error'; + +const initialState = { + showError: false, + formData: { + confirmPassword: '', + }, + errors: {}, +}; class ChangeEmailContentDialog extends React.Component { - state = { - showError: false, + state = initialState; + + clearForm = () => { + this.setState(initialState); + }; + + addError = err => { + this.setState(({ errors }) => ({ + errors: { ...errors, ...err }, + })); + }; + + removeError = errKey => { + this.setState(state => { + const { [errKey]: _, ...errors } = state.errors; + return { + errors, + }; + }); + }; + + fieldValidation = (value, type, name) => { + if (!value.length) { + this.addError({ + [name]: t('talk-plugin-local-auth.change_password.required_field'), + }); + } else if (!validate[type](value)) { + this.addError({ [name]: errorMsj[type] }); + } else { + this.removeError(name); + } + }; + + onChange = e => { + const { name, value, type, dataset } = e.target; + const validationType = dataset.validationType || type; + + this.setState( + state => ({ + formData: { + ...state.formData, + [name]: value, + }, + }), + () => { + this.fieldValidation(value, validationType, name); + } + ); + }; + + hasError = err => { + return Object.keys(this.state.errors).indexOf(err) !== -1; + }; + + getError = errorKey => { + return this.state.errors[errorKey]; }; showError = () => { @@ -16,24 +80,31 @@ class ChangeEmailContentDialog extends React.Component { }); }; + cancel = () => { + this.clearForm(); + this.props.closeDialog(); + }; + confirmChanges = async e => { e.preventDefault(); + const { confirmPassword = '' } = this.state.formData; + if (this.formHasError()) { this.showError(); return; } - await this.props.save(); + await this.props.save(confirmPassword); this.props.next(); }; - formHasError = () => this.props.hasError('confirmPassword'); + formHasError = () => this.hasError('confirmPassword'); render() { return (
    - + ×

    @@ -59,17 +130,17 @@ class ChangeEmailContentDialog extends React.Component { label={t('talk-plugin-local-auth.change_email.enter_password')} name="confirmPassword" type="password" - onChange={this.props.onChange} - defaultValue="" - hasError={this.props.hasError('confirmPassword')} - errorMsg={this.props.getError('confirmPassword')} + onChange={this.onChange} + value={this.state.formData.confirmPassword} + hasError={this.hasError('confirmPassword')} + errorMsg={this.getError('confirmPassword')} showError={this.state.showError} columnDisplay />
    - -
    - - - - \ No newline at end of file diff --git a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk new file mode 100644 index 000000000..5080277f0 --- /dev/null +++ b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk @@ -0,0 +1,68 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %} + +{% block css %} +{{ super() }} + +{% endblock %} + +{% block html %} +
    +
    {{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}
    +
    {{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}
    +
    + {{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }} + +
    +
    +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js index bbada134f..c2055a16e 100644 --- a/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js +++ b/plugins/talk-plugin-profile-data/client/components/DeleteMyAccount.js @@ -27,7 +27,7 @@ class DeleteMyAccount extends React.Component { const { cancelAccountDeletion, notify } = this.props; try { await cancelAccountDeletion(); - notify('success', t('delete_request.account_deletion_requested')); + notify('success', t('delete_request.account_deletion_cancelled')); } catch (err) { notify('error', getErrorMessages(err)); } @@ -63,17 +63,18 @@ class DeleteMyAccount extends React.Component {

    {t('delete_request.delete_my_account_description')}

    -

    - {scheduledDeletionDate && - t( - 'delete_request.already_submitted_request_description', - moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a') - )} -

    {scheduledDeletionDate ? ( - +
    +

    + {t( + 'delete_request.already_submitted_request_description', + moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a') + )} +

    + +
    ) : ( - - -

    - - - - diff --git a/plugins/talk-plugin-profile-data/server/views/download.njk b/plugins/talk-plugin-profile-data/server/views/download.njk new file mode 100644 index 000000000..ee9c94272 --- /dev/null +++ b/plugins/talk-plugin-profile-data/server/views/download.njk @@ -0,0 +1,56 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('download_landing.download_your_account') }}{% endblock %} + +{% block html %} +
    +
    +

    {{ t('download_landing.download_your_account') }}

    +

    {{ t('download_landing.download_details') }}

    +

    {{ t('download_landing.all_information_included') }}

    +
      +
    • {{ t('download_landing.information_included.date') }}
    • +
    • {{ t('download_landing.information_included.url') }}
    • +
    • {{ t('download_landing.information_included.body') }}
    • +
    • {{ t('download_landing.information_included.asset_url') }}
    • +
    +
    +
    + +
    +
    +
    +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/plugins/talk-plugin-profile-data/translations.yml b/plugins/talk-plugin-profile-data/translations.yml index 39b810cca..c3b7e40ca 100644 --- a/plugins/talk-plugin-profile-data/translations.yml +++ b/plugins/talk-plugin-profile-data/translations.yml @@ -7,7 +7,7 @@ en: date: "When you wrote the comment" url: "The permalink URL for the comment" body: "The comment text" - asset_url: "The URL on the article or story where the comment appears" + asset_url: "The URL of the article or story where the comment appears" confirm: "Download My Comment History" email: download: @@ -17,7 +17,7 @@ en: delete: subject: "Your account for {0} is scheduled to be deleted" body: | - A request to delete your account was received. Your account is scheduled for deletion on {1}. + A request to delete your account was received. Your account is scheduled for deletion on {1}. After that time all of your comments will be removed from the site, all of your comments will be removed from our database, and your username and email address will be removed from our system. @@ -35,3 +35,40 @@ en: body: "You have cancelled your account deletion request for {0}. Your account is now reactivated." error: DOWNLOAD_TOKEN_INVALID: "Your download link is not valid." +de: + download_landing: + download_your_account: "Mein Kommentar-Archiv herunterladen" + download_details: "Ihr Kommentar-Archiv wird als ZIP-Datei bereitgestellt. Nach dem Entpacken erhalten Sie eine CSV-Datei, die einfach in ein Tabellenkalkulationsprogramm importiert werden kann." + all_information_included: "Für jeden Ihrer Kommentare sind folgende Informationen enthalten:" + information_included: + date: "Wann Sie den Kommentar geschrieben haben" + url: "Die dauerhafte URL (Internetadresse) des Kommentars" + body: "Der Kommentar-Text" + asset_url: "Die URL (Internetadresse) des Artikels an dem der Kommentar erscheint" + confirm: "Kommentar-Archiv herunterladen" + email: + download: + subject: "Ihre Kommentare sind zum Download bereit: {0}" + download_link_ready: "Hier klicken, um Ihre Kommentare von {0} bis {1} herunterzuladen:" + download_archive: "Archiv herunterladen" + delete: + subject: "Ihr Benutzerkonto bei {0} ist zur Löschung vorgesehen" + body: | + Wir haben eine Anfrage erhalten, Ihr Benutzerkonto zu löschen. Die Löschung ist geplant für den {1}. + + Nach diesem Zeitpunkt werden alle Ihre Kommentare von der Website und aus unserer Datenbank gelöscht. Außerdem werden Ihr Nutzername und Ihre E-Mail-Adresse aus unserem System enfernt. + + Falls Sie es sich noch anders überlegen, können Sie sich bis spätestens zum angegebenen Lösch-Zeitpunkt einloggen und die Lösch-Anfrage abbrechen. + deleted: + subject: "Ihre Benutzerkonto bei {0} wurde gelöscht" + body: | + Ihr Kommentar-Konto bei {0} ist nun gelöscht. Schade, auf Wiedersehen! + + Falls Sie sich in Zukunft erneut an der Diskussion beteiligen möchten, können Sie jederzeit ein neues Benutzerkonto einrichten. + + Wenn Sie Lust haben, schreiben Sie uns doch eine Rückmeldung, Feedback, oder Kritik an {1}, damit wir unsere Community verbessern können. Vielen Dank! + cancelDelete: + subject: "Die Lösch-Anfrage für Ihr Benutzerkonto bei {0} wurde abgebrochen" + body: "Sie haben die Lösch-Anfrage für Ihr Benutzerkonto bei {0} abgebrochen. Das Konto ist nun wieder aktiv." + error: + DOWNLOAD_TOKEN_INVALID: "Der Download-Link ist ungültig." diff --git a/plugins/talk-plugin-rich-text/client/components/Editor.css b/plugins/talk-plugin-rich-text/client/components/Editor.css index 2259d01a3..be2646121 100644 --- a/plugins/talk-plugin-rich-text/client/components/Editor.css +++ b/plugins/talk-plugin-rich-text/client/components/Editor.css @@ -1,5 +1,12 @@ +@custom-media --narrow-viewport (max-width: 420px); + .commentContent { composes: content from "./CommentContent.css"; + + /* Prevent zoom on narrow viewports */ + @media (--narrow-viewport) { + font-size: 16px; + } } .placeholder { diff --git a/plugins/talk-plugin-rich-text/client/components/Editor.js b/plugins/talk-plugin-rich-text/client/components/Editor.js index e09931d38..99d2be617 100644 --- a/plugins/talk-plugin-rich-text/client/components/Editor.js +++ b/plugins/talk-plugin-rich-text/client/components/Editor.js @@ -8,6 +8,7 @@ import RTE from './rte/RTE'; import { Icon } from 'plugin-api/beta/client/components/ui'; import { Bold, Italic, Blockquote } from './rte/features'; import { t } from 'plugin-api/beta/client/services'; +import bowser from 'bowser'; class Editor extends React.Component { ref = null; @@ -40,7 +41,9 @@ class Editor extends React.Component { } }); } - if (this.props.isReply) { + + // Skip IOS due to a bug, see https://www.pivotaltracker.com/story/show/157434928 + if (this.props.isReply && !bowser.ios) { this.ref.focus(); } } diff --git a/plugins/talk-plugin-rich-text/client/components/rte/RTE.css b/plugins/talk-plugin-rich-text/client/components/rte/RTE.css index 2bf1feaf5..683388707 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/RTE.css +++ b/plugins/talk-plugin-rich-text/client/components/rte/RTE.css @@ -15,6 +15,8 @@ position: absolute; margin: 12px 0 0 12px; color: #bbb; + user-select: none; + pointer-events: none; } .toolbarDisabled { diff --git a/plugins/talk-plugin-rich-text/client/components/rte/components/Button.js b/plugins/talk-plugin-rich-text/client/components/rte/components/Button.js index f99648cb7..fd5d451a1 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/components/Button.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/components/Button.js @@ -7,21 +7,17 @@ class Button extends React.Component { render() { const { className, - title, - onClick, children, active, activeClassName, - disabled, + ...rest } = this.props; return ( @@ -32,11 +28,8 @@ class Button extends React.Component { Button.propTypes = { className: PropTypes.string, activeClassName: PropTypes.string, - title: PropTypes.string, - onClick: PropTypes.func, children: PropTypes.node, active: PropTypes.bool, - disabled: PropTypes.bool, }; export default Button; diff --git a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js index 028757222..1fe8641f6 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js @@ -62,6 +62,7 @@ const createToggle = ( - - -
    - - - - diff --git a/views/account/email/confirm.njk b/views/account/email/confirm.njk new file mode 100644 index 000000000..8e2c9ff05 --- /dev/null +++ b/views/account/email/confirm.njk @@ -0,0 +1,63 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('confirm_email.email_confirmation') }}{% endblock %} + +{% block html %} +
    +
    +

    {{ t('confirm_email.email_confirmation') }}

    +

    {{ t('confirm_email.click_to_confirm') }}

    +
    +
    + +
    +
    +
    +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/account/password/reset.ejs b/views/account/password/reset.ejs deleted file mode 100644 index c8b106139..000000000 --- a/views/account/password/reset.ejs +++ /dev/null @@ -1,85 +0,0 @@ - - - - <%= t('password_reset.set_new_password') %> - <%- include ../../partials/account %> - - -
    -
    -

    <%= t('password_reset.set_new_password') %>

    -

    <%= t('password_reset.change_password_help') %>

    -
    -
    - - - -
    -
    -
    - - - - diff --git a/views/account/password/reset.njk b/views/account/password/reset.njk new file mode 100644 index 000000000..6eee12dd0 --- /dev/null +++ b/views/account/password/reset.njk @@ -0,0 +1,85 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('password_reset.set_new_password') }}{% endblock %} + +{% block html %} +
    +
    +

    {{ t('password_reset.set_new_password') }}

    +

    {{ t('password_reset.change_password_help') }}

    +
    +
    + + + +
    +
    +
    +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/admin.ejs b/views/admin.ejs deleted file mode 100644 index b8f775d79..000000000 --- a/views/admin.ejs +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Talk - Coral Admin - - - - - - <%- include partials/head %> - - - -
    - - - - diff --git a/views/admin.njk b/views/admin.njk new file mode 100644 index 000000000..1b60f6e76 --- /dev/null +++ b/views/admin.njk @@ -0,0 +1,14 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk Admin{% endblock %} + +{% block css %} + + + +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/api/graphiql.ejs b/views/api/graphiql.ejs deleted file mode 100644 index b6d609de6..000000000 --- a/views/api/graphiql.ejs +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - GraphiQL - - - - - - - - - - - - \ No newline at end of file diff --git a/views/api/graphiql.njk b/views/api/graphiql.njk new file mode 100644 index 000000000..b726918f3 --- /dev/null +++ b/views/api/graphiql.njk @@ -0,0 +1,126 @@ +{% extends "templates/development.njk" %} + +{% block title %}GraphiQL{% endblock %} + +{% block css %} +{# Include the base development pieces #} +{{ super() }} + + +{% endblock %} + +{% block js %} + + + + + +{% endblock %} + +{% block html %} +
    +{% endblock %} diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs deleted file mode 100644 index e4d2387b7..000000000 --- a/views/auth-callback.ejs +++ /dev/null @@ -1,10 +0,0 @@ - - - - <%- include partials/data %> - - - - - - diff --git a/views/auth-callback.njk b/views/auth-callback.njk new file mode 100644 index 000000000..cfd439ea0 --- /dev/null +++ b/views/auth-callback.njk @@ -0,0 +1,10 @@ + + + + {% include "partials/data.njk" %} + + + + + + diff --git a/views/dev/article.ejs b/views/dev/article.ejs deleted file mode 100644 index 8abfc34af..000000000 --- a/views/dev/article.ejs +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - <%= title %> - - -
    -

    <%= title %>

    -

    <%= body %>

    -

    Admin - All Assets

    -
    - -
    - - diff --git a/views/dev/article.njk b/views/dev/article.njk new file mode 100644 index 000000000..2fb8b3724 --- /dev/null +++ b/views/dev/article.njk @@ -0,0 +1,55 @@ +{% extends "templates/development.njk" %} + +{% block title %}{{ title }}{% endblock %} + +{% block meta %} + + + + + + + + +{% endblock %} + +{% block html %} +
    +

    {{ title }}

    +
    +
    +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/dev/articles.ejs b/views/dev/articles.ejs deleted file mode 100644 index 0ce684029..000000000 --- a/views/dev/articles.ejs +++ /dev/null @@ -1,14 +0,0 @@ - - -

    - Asset list -

    -<% assets.forEach(function (asset) { %> - <%= asset.url %>
    -<% }) %> -

    - (For dev use only. FYI, you can: ?skip=100&limit=25) -

    - - - diff --git a/views/dev/articles.njk b/views/dev/articles.njk new file mode 100644 index 000000000..e204c39fc --- /dev/null +++ b/views/dev/articles.njk @@ -0,0 +1,37 @@ +{% extends "templates/development.njk" %} + +{% block title %}All Assets{% endblock %} + +{% block html %} +
    +
    +

    All Assets

    + {{ skip + 1 }} - {{ skip + assets.length }} of {{ count }} Assets +
    + + {% if count !== assets.length %} + + {% endif %} +
    +{% endblock %} diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs deleted file mode 100644 index 2027f6069..000000000 --- a/views/embed/stream.ejs +++ /dev/null @@ -1,13 +0,0 @@ - - - - - <%- include ../partials/head %> - - - - -
    - - - diff --git a/views/embed/stream.njk b/views/embed/stream.njk new file mode 100644 index 000000000..9b237b5c2 --- /dev/null +++ b/views/embed/stream.njk @@ -0,0 +1,19 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk{% endblock %} + +{% block css %} + + + +{# Custom CSS is included after the CSS block so that its overrides will apply #} +{% include "partials/custom-css.njk" %} +{% endblock %} + +{% block html %} +
    +{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/views/login.ejs b/views/login.ejs deleted file mode 100644 index c5a86c2fb..000000000 --- a/views/login.ejs +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - <%- include partials/head %> - - - -
    - - - - diff --git a/views/login.njk b/views/login.njk new file mode 100644 index 000000000..7198dbdff --- /dev/null +++ b/views/login.njk @@ -0,0 +1,19 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk - Login{% endblock %} + +{% block css %} + + +{# Custom CSS is included after the CSS block so that its overrides will apply #} +{% include "partials/custom-css.njk" %} +{% endblock %} + +{% block html %} +
    +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/partials/account.ejs b/views/partials/account.ejs deleted file mode 100644 index 79857baf0..000000000 --- a/views/partials/account.ejs +++ /dev/null @@ -1,3 +0,0 @@ -<%- include ./head %> - - diff --git a/views/partials/custom-css.njk b/views/partials/custom-css.njk new file mode 100644 index 000000000..4e5a494e6 --- /dev/null +++ b/views/partials/custom-css.njk @@ -0,0 +1,3 @@ +{% if customCssUrl %} + +{% endif %} diff --git a/views/partials/data.ejs b/views/partials/data.ejs deleted file mode 100644 index f0bbf5925..000000000 --- a/views/partials/data.ejs +++ /dev/null @@ -1,3 +0,0 @@ -<%_ if (data != null) { _%> - -<%_ } _%> \ No newline at end of file diff --git a/views/partials/data.njk b/views/partials/data.njk new file mode 100644 index 000000000..4edaf2e2e --- /dev/null +++ b/views/partials/data.njk @@ -0,0 +1,3 @@ +{% if data %} + +{% endif %} diff --git a/views/partials/favicon.njk b/views/partials/favicon.njk new file mode 100644 index 000000000..5ec2aad8a --- /dev/null +++ b/views/partials/favicon.njk @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/views/partials/head.ejs b/views/partials/head.ejs deleted file mode 100644 index e848b3909..000000000 --- a/views/partials/head.ejs +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -<%_ if (locals.customCssUrl) { _%> - -<%_ } _%> -<%- include data %> - - diff --git a/views/partials/nav.njk b/views/partials/nav.njk new file mode 100644 index 000000000..ecea9018d --- /dev/null +++ b/views/partials/nav.njk @@ -0,0 +1,8 @@ + diff --git a/views/templates/account.njk b/views/templates/account.njk new file mode 100644 index 000000000..bcda22a67 --- /dev/null +++ b/views/templates/account.njk @@ -0,0 +1,9 @@ +{% extends "templates/base.njk" %} + +{% block css %} + + + +{# Custom CSS is included after the CSS block so that its overrides will apply #} +{% include "partials/custom-css.njk" %} +{% endblock %} diff --git a/views/templates/base.njk b/views/templates/base.njk new file mode 100644 index 000000000..3a5331a1d --- /dev/null +++ b/views/templates/base.njk @@ -0,0 +1,38 @@ + + + + {# Meta tags #} + + + {% block meta %}{% endblock %} + + {# Favicon Configuration #} + {% include "partials/favicon.njk" %} + + {# CSP and security headers #} + {% block security %}{% endblock %} + + {# Title #} + {% block title %}{% endblock %} + + {# CSS #} + + + + {% block css %}{% endblock %} + + {# Static data injection #} + {% include "partials/data.njk" %} + + {# Configuration #} + + + + {% block body %} + {% block html %} +
    + {% endblock %} + {% endblock %} + {% block js %}{% endblock %} + + diff --git a/views/templates/development.njk b/views/templates/development.njk new file mode 100644 index 000000000..f7fab6682 --- /dev/null +++ b/views/templates/development.njk @@ -0,0 +1,17 @@ +{% extends "templates/base.njk" %} + +{# Null out the security block, we don't want/need that in development #} +{% block security %}{% endblock %} + +{% block css %} + + + + +{% endblock %} + +{% block body %} + {% include "partials/nav.njk" %} + + {% block html %}{% endblock %} +{% endblock %} diff --git a/webpack.config.js b/webpack.config.js index c6b3a770f..3d815e53b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -169,6 +169,7 @@ const config = { TALK_REPLY_COMMENTS_LOAD_DEPTH: '3', TALK_DEFAULT_STREAM_TAB: 'all', TALK_DEFAULT_LANG: 'en', + TALK_WHITELISTED_LANGUAGES: '', }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), @@ -308,7 +309,10 @@ const applyConfig = (entries, root = {}) => entry: entries.reduce( (entry, { name, path: modulePath, disablePolyfill = false }) => { const entries = [ - path.join(__dirname, 'client/coral-framework/helpers/publicPath'), + path.join( + __dirname, + 'client/coral-framework/helpers/webpackGlobals' + ), ]; if (disablePolyfill) { entries.push(modulePath); diff --git a/yarn.lock b/yarn.lock index a043febf4..0f6606433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -167,7 +167,7 @@ abbrev@1, abbrev@^1.0.7: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" -accepts@^1.3.4, accepts@~1.3.4: +accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: @@ -649,12 +649,18 @@ async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.4.1, async@~2.6.0: +async@^2.0.0, async@^2.1.2, async@^2.4.1, async@~2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: lodash "^4.14.0" +async@^2.1.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + dependencies: + lodash "^4.17.10" + async@~0.2.6: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" @@ -690,7 +696,11 @@ aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" -aws4@^1.2.1, aws4@^1.6.0: +aws4@^1.2.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + +aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -1413,10 +1423,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" bl@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" dependencies: - readable-stream "^2.0.5" + readable-stream "^2.3.5" + safe-buffer "^5.1.1" block-stream@*: version "0.0.9" @@ -1644,6 +1655,21 @@ bson@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c" +bson@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.6.tgz#444db59ddd4c24f0cb063aabdc5c8c7b0ceca912" + +buffer-alloc-unsafe@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a" + +buffer-alloc@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303" + dependencies: + buffer-alloc-unsafe "^0.1.0" + buffer-fill "^0.1.0" + buffer-crc32@^0.2.1, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -1652,9 +1678,9 @@ buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" -buffer-shims@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +buffer-fill@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.1.tgz#76d825c4d6e50e06b7a31eb520c04d08cc235071" buffer-xor@^1.0.3: version "1.0.3" @@ -1828,6 +1854,13 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +casual@^1.5.19: + version "1.5.19" + resolved "https://registry.yarnpkg.com/casual/-/casual-1.5.19.tgz#66fac46f7ae463f468f5913eb139f9c41c58bbf2" + dependencies: + mersenne-twister "^1.0.1" + moment "^2.15.2" + center-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" @@ -1970,6 +2003,24 @@ chokidar@^1.5.2, chokidar@^1.6.0, chokidar@^1.6.1, chokidar@^1.7.0: optionalDependencies: fsevents "^1.0.0" +chokidar@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176" + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "^1.0.0" + optionalDependencies: + fsevents "^1.1.2" + chokidar@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.2.tgz#4dc65139eeb2714977735b6a35d06e97b494dfd7" @@ -2063,11 +2114,14 @@ cli-spinners@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" +cli-table2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" dependencies: - colors "1.0.3" + lodash "^3.10.1" + string-width "^1.0.1" + optionalDependencies: + colors "^1.1.2" cli-truncate@^0.2.1: version "0.2.1" @@ -2197,7 +2251,7 @@ colors@0.5.x: version "0.5.1" resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774" -colors@1.0.3, colors@1.0.x: +colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -2210,8 +2264,8 @@ colors@^1.1.2, colors@~1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" @@ -2231,7 +2285,7 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" -commander@^2.11.0, commander@^2.9.0: +commander@^2.11.0: version "2.12.2" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" @@ -2239,6 +2293,10 @@ commander@^2.14.1: version "2.14.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" +commander@^2.9.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -2385,6 +2443,12 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +consolidate@0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.0.tgz#b03acd566a2565ca96e99f44fd1417486b4df88d" + dependencies: + bluebird "^3.1.1" + constantinople@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.0.tgz#7569caa8aa3f8d5935d62e1fa96f9f702cd81c79" @@ -2596,6 +2660,16 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -2821,7 +2895,7 @@ debug@*, debug@3.1.0, debug@^3.0.0, debug@^3.0.1, debug@^3.1.0: dependencies: ms "2.0.0" -debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: +debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -3346,10 +3420,6 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" -es6-promise@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4" - es6-promise@^4.0.5: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" @@ -3972,6 +4042,10 @@ flexbuffer@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" +fluent-langneg@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fluent-langneg/-/fluent-langneg-0.1.0.tgz#aa12054fbfa4b728daec38331efc12f01faae93a" + flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" @@ -4066,6 +4140,10 @@ from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + fs-extra@^0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" @@ -4093,6 +4171,12 @@ fs-extra@^4.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + fs-promise@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-0.3.1.tgz#bf34050368f24d6dc9dfc6688ab5cead8f86842a" @@ -4123,6 +4207,13 @@ fsevents@^1.0.0, fsevents@^1.1.1: nan "^2.3.0" node-pre-gyp "^0.6.39" +fsevents@^1.1.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" + dependencies: + nan "^2.9.2" + node-pre-gyp "^0.10.0" + fstream-ignore@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" @@ -4451,6 +4542,10 @@ graphql-extensions@^0.0.x: core-js "^2.5.1" source-map-support "^0.5.0" +graphql-fields@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/graphql-fields/-/graphql-fields-1.0.2.tgz#099ee1d4445b42d0f47e06d622acebb33abc6cce" + graphql-redis-subscriptions@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.3.0.tgz#bbc52b0f77bf7d50945c6bf4e8b8aba5135555b4" @@ -4879,10 +4974,6 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -hooks-fixed@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-2.0.2.tgz#20076daa07e77d8a6106883ce3f1722e051140b0" - hosted-git-info@^2.1.4: version "2.5.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" @@ -5006,6 +5097,12 @@ iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@^0.4.4: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -5038,6 +5135,12 @@ ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + ignore@^3.3.3, ignore@^3.3.5: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" @@ -5409,12 +5512,17 @@ is-ip@1.0.0: dependencies: ip-regex "^1.0.0" +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + is-my-json-valid@^2.12.4: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" + version "2.17.2" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c" dependencies: generate-function "^2.0.0" generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" jsonpointer "^4.0.0" xtend "^4.0.0" @@ -6388,9 +6496,9 @@ jxLoader@*: promised-io "*" walker "1.x" -kareem@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.5.0.tgz#e3e4101d9dcfde299769daf4b4db64d895d17448" +kareem@2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.0.7.tgz#8d260366a4df4236ceccec318fcf10c17c5beb22" keymaster@^1.6.2: version "1.6.2" @@ -6517,9 +6625,9 @@ linkifyjs@^2.1.5: react ">=0.14.0" react-dom ">=0.14.0" -lint-staged@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.0.0.tgz#57926c63201e7bd38ca0576d74391efa699b4a9d" +lint-staged@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.1.0.tgz#1514a5b71b8d9492ca0c3d2a44769cbcbc8bcc79" dependencies: app-root-path "^2.0.1" chalk "^2.3.1" @@ -6530,6 +6638,7 @@ lint-staged@^7.0.0: execa "^0.9.0" find-parent-dir "^0.3.0" is-glob "^4.0.0" + is-windows "^1.0.2" jest-validate "^22.4.0" listr "^0.13.0" lodash "^4.17.5" @@ -6539,8 +6648,9 @@ lint-staged@^7.0.0: p-map "^1.1.1" path-is-inside "^1.0.2" pify "^3.0.0" - please-upgrade-node "^3.0.1" - staged-git-files "1.1.0" + please-upgrade-node "^3.0.2" + staged-git-files "1.1.1" + string-argv "^0.0.2" stringify-object "^3.2.2" listr-silent-renderer@^1.1.1: @@ -6932,11 +7042,15 @@ lodash.values@^4.3.0: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" -lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1: +lodash@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +lodash@^4.0.0, lodash@^4.1.0, lodash@^4.2.0, lodash@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^4.17.10: +lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -7150,6 +7264,10 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" +mersenne-twister@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" + metascraper-author@^3.9.2: version "3.9.2" resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-3.9.2.tgz#ff2020ac428f59a875d655df3b0d4bea171fde19" @@ -7290,16 +7408,22 @@ miller-rabin@^4.0.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" -"mime-db@>= 1.33.0 < 2": +"mime-db@>= 1.33.0 < 2", mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: +mime-types@^2.1.10, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: mime-db "~1.30.0" +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + mime@1.4.1, mime@^1.3.4, mime@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" @@ -7340,6 +7464,19 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + mississippi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" @@ -7432,36 +7569,40 @@ moment@^2.10.3: version "2.19.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" -mongodb-core@2.1.17: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.17.tgz#a418b337a14a14990fb510b923dee6a813173df8" +moment@^2.15.2: + version "2.22.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad" + +mongodb-core@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.0.8.tgz#8d401f4eab6056c0d874a3d5844a4844f761d4d7" dependencies: bson "~1.0.4" - require_optional "~1.0.0" + require_optional "^1.0.1" -mongodb@2.2.33: - version "2.2.33" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.33.tgz#b537c471d34a6651b48f36fdbf29750340e08b50" +mongodb@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.0.8.tgz#2c1daecac9a0ec2de2f2aea4dc97d76ae70f8951" dependencies: - es6-promise "3.2.1" - mongodb-core "2.1.17" - readable-stream "2.2.7" + mongodb-core "3.0.8" -mongoose@^4.12.3: - version "4.13.7" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.13.7.tgz#f760c770e6c8cdf34a6fe8b7443882b5fced1032" +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + +mongoose@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.1.1.tgz#a7e925607e76032e5ef20b3035a357bc8581b45e" dependencies: async "2.1.4" - bson "~1.0.4" - hooks-fixed "2.0.2" - kareem "1.5.0" + bson "~1.0.5" + kareem "2.0.7" lodash.get "4.4.2" - mongodb "2.2.33" - mpath "0.3.0" - mpromise "0.5.5" - mquery "2.3.3" + mongodb "3.0.8" + mongoose-legacy-pluralize "1.0.2" + mpath "0.4.1" + mquery "3.0.0" ms "2.0.0" - muri "1.3.0" regexp-clone "0.0.1" sliced "1.0.1" @@ -7490,17 +7631,13 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -mpath@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.3.0.tgz#7a58f789e9b5fd3c94520634157960f26bd5ef44" +mpath@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.4.1.tgz#ed10388430380bf7bbb5be1391e5d6969cb08e89" -mpromise@0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6" - -mquery@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-2.3.3.tgz#221412e5d4e7290ca5582dd16ea8f190a506b518" +mquery@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.0.0.tgz#e5f387dbabc0b9b69859e550e810faabe0ceabb0" dependencies: bluebird "3.5.0" debug "2.6.9" @@ -7519,10 +7656,6 @@ ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" -muri@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/muri/-/muri-1.3.0.tgz#aeccf3db64c56aa7c5b34e00f95b7878527a4721" - murmurhash-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" @@ -7551,6 +7684,10 @@ nan@^2.3.2: version "2.9.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866" +nan@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + nanomatch@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" @@ -7605,6 +7742,14 @@ nearley@^2.7.10: railroad-diagrams "^1.0.0" randexp "^0.4.2" +needle@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -7619,6 +7764,10 @@ nib@~1.1.2: dependencies: stylus "0.54.5" +nice-try@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" + nightwatch@^0.9.16: version "0.9.19" resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-0.9.19.tgz#4bd9757273d30b845f04847a98b71be9bb7c4b3b" @@ -7744,6 +7893,21 @@ node-notifier@^5.2.1: shellwords "^0.1.1" which "^1.3.0" +node-pre-gyp@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.0" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.1.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + node-pre-gyp@^0.6.39: version "0.6.39" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" @@ -7950,6 +8114,17 @@ normalize-url@~2.0.0: query-string "^5.0.1" sort-keys "^2.0.0" +npm-bundled@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" + +npm-packlist@^1.1.6: + version "1.1.10" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-path@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64" @@ -8018,6 +8193,17 @@ nunjucks@^3.1.2: optionalDependencies: chokidar "^1.6.0" +nunjucks@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.1.3.tgz#9a23c844af01c143a0b40f3bdd1212a9d7877260" + dependencies: + a-sync-waterfall "^1.0.0" + asap "^2.0.3" + postinstall-build "^5.0.1" + yargs "^3.32.0" + optionalDependencies: + chokidar "^2.0.0" + "nwmatcher@>= 1.3.7 < 2.0.0", nwmatcher@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" @@ -8429,7 +8615,7 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" -path-key@^2.0.0: +path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -8537,9 +8723,11 @@ platform@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.4.tgz#6f0fb17edaaa48f21442b3a975c063130f1c3ebd" -please-upgrade-node@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.0.1.tgz#0a681f2c18915e5433a5ca2cd94e0b8206a782db" +please-upgrade-node@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.0.2.tgz#7b9eaeca35aa4a43d6ebdfd10616c042f9a83acc" + dependencies: + semver-compare "^1.0.0" pluralize@^1.2.1: version "1.2.1" @@ -9656,7 +9844,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.1.5, readable-stream@^2.2.2: +"readable-stream@1 || 2", readable-stream@^2.0.4, readable-stream@^2.1.5, readable-stream@^2.2.2: version "2.3.5" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" dependencies: @@ -9686,7 +9874,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@2, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.3.3: +readable-stream@2, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -9698,16 +9886,16 @@ readable-stream@2, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stre string_decoder "~1.0.3" util-deprecate "~1.0.1" -readable-stream@2.2.7: - version "2.2.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1" +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: - buffer-shims "~1.0.0" core-util-is "~1.0.0" - inherits "~2.0.1" + inherits "~2.0.3" isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" util-deprecate "~1.0.1" readdirp@^2.0.0: @@ -9940,31 +10128,6 @@ request@2, request@^2.55.0, request@^2.74.0, request@^2.81.0, request@^2.83.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@2.79.0, request@~2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" - request@2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -9992,6 +10155,56 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@2.87.0: + version "2.87.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +request@~2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -10015,7 +10228,7 @@ require-uncached@^1.0.3: caller-path "^0.1.0" resolve-from "^1.0.0" -require_optional@~1.0.0: +require_optional@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" dependencies: @@ -10152,10 +10365,14 @@ rxjs@^5.4.2: dependencies: symbol-observable "1.0.1" -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + safe-json-stringify@~1: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz#bd2b6dad1ebafab3c24672a395527f01804b7e19" @@ -10166,6 +10383,10 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + samsam@1.1.2, samsam@~1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -10249,30 +10470,34 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" selenium-standalone@^6.11.0: - version "6.12.0" - resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.12.0.tgz#789730db09a105f1cce12c6424d795d11c543bd4" + version "6.15.0" + resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.15.0.tgz#c8dc77bd45154afbda7509ec53dc599809f63ee4" dependencies: async "^2.1.4" commander "^2.9.0" - cross-spawn "^5.1.0" + cross-spawn "^6.0.0" debug "^3.0.0" lodash "^4.17.4" minimist "^1.2.0" mkdirp "^0.5.1" progress "2.0.0" - request "2.79.0" - tar-stream "1.5.2" + request "2.87.0" + tar-stream "1.6.1" urijs "^1.18.4" which "^1.2.12" yauzl "^2.5.0" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -10716,8 +10941,8 @@ srcset@^1.0.0: number-is-nan "^1.0.0" sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -10743,9 +10968,9 @@ stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" -staged-git-files@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.0.tgz#1a9bb131c1885601023c7aaddd3d54c22142c526" +staged-git-files@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.1.tgz#37c2218ef0d6d26178b1310719309a16a59f8f7b" static-extend@^0.1.1: version "0.1.2" @@ -10822,6 +11047,10 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" +string-argv@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736" + string-hash@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" @@ -10856,7 +11085,7 @@ string.prototype.padend@^3.0.0: es-abstract "^1.4.3" function-bind "^1.0.2" -string_decoder@^1.0.0, string_decoder@~1.0.0, string_decoder@~1.0.3: +string_decoder@^1.0.0, string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: @@ -10866,6 +11095,12 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + stringify-object@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd" @@ -10874,7 +11109,11 @@ stringify-object@^3.2.2: is-obj "^1.0.1" is-regexp "^1.0.0" -stringstream@~0.0.4, stringstream@~0.0.5: +stringstream@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" + +stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -11106,7 +11345,19 @@ tar-pack@^3.4.0: tar "^2.2.1" uid-number "^0.0.6" -tar-stream@1.5.2, tar-stream@^1.1.2: +tar-stream@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395" + dependencies: + bl "^1.0.0" + buffer-alloc "^1.1.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.0" + xtend "^4.0.0" + +tar-stream@^1.1.2: version "1.5.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.2.tgz#fbc6c6e83c1a19d4cb48c7d96171fc248effc7bf" dependencies: @@ -11132,6 +11383,18 @@ tar@^2.0.0, tar@^2.2.1: fstream "^1.0.2" inherits "2" +tar@^4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + tcomb@^2.5.1: version "2.7.0" resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0" @@ -11253,6 +11516,10 @@ to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" +to-buffer@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + to-capital-case@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-capital-case/-/to-capital-case-1.0.0.tgz#a57c5014fd5a37217cf05099ff8a421bbf9c9b7f" @@ -11335,12 +11602,18 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@>=2.3.3, tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3: +tough-cookie@>=2.3.3, tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" +tough-cookie@~2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + tr46@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -11604,8 +11877,8 @@ upper-case@^1.1.1: resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" urijs@^1.18.4: - version "1.19.0" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.0.tgz#d8aa284d0e7469703a6988ad045c4cbfdf08ada0" + version "1.19.1" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" urix@^0.1.0: version "0.1.0" @@ -11870,12 +12143,18 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@1, which@^1.2.10, which@^1.2.12, which@^1.2.9, which@^1.3.0: +which@1, which@^1.2.10, which@^1.2.9, which@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: isexe "^2.0.0" +which@^1.2.12: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" @@ -12028,6 +12307,10 @@ yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + yaml-lint@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yaml-lint/-/yaml-lint-1.0.0.tgz#655068f583263eec7e2a0ded8f5b1895dd667e1c"