mirror of
https://github.com/wassname/talk.git
synced 2026-07-24 13:20:47 +08:00
Merge branch 'master' into neutral-reactions-icon
This commit is contained in:
@@ -2,5 +2,10 @@
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "15.0"
|
||||
}
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
overrides:
|
||||
- files: "bin/cli*"
|
||||
options:
|
||||
parser: babylon
|
||||
+2
-1
@@ -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
|
||||
yarn cache clean
|
||||
|
||||
@@ -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');
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"MAILGUN_SMTP_PASSWORD": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
|
||||
"NPM_CONFIG_PRODUCTION": "false"
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
+1
-2
@@ -329,8 +329,7 @@ async function createSeedPlugin() {
|
||||
if (answers.addPluginsJson) {
|
||||
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
|
||||
|
||||
fs
|
||||
.readJson(pluginsJson)
|
||||
fs.readJson(pluginsJson)
|
||||
.then(j => {
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
|
||||
+5
-2
@@ -8,7 +8,7 @@ const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const Table = require('cli-table');
|
||||
const Table = require('cli-table2');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -49,7 +49,10 @@ async function revokeToken(tokenID) {
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
|
||||
let {
|
||||
pat: { id },
|
||||
jwt,
|
||||
} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
export const showRejectUsernameDialog = ({ userId, username }) => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
@@ -76,7 +76,7 @@ ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
buttonClassNames: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const ActionsMenuItem = props => (
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from './NotFound.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -41,13 +42,13 @@ class AdminLogin extends React.Component {
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
label={t('login.password')}
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
type="password"
|
||||
@@ -60,10 +61,10 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleSignIn}
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in_button')}
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
@@ -72,7 +73,7 @@ class AdminLogin extends React.Component {
|
||||
this.setState({ requestPassword: true });
|
||||
}}
|
||||
>
|
||||
Request a new one.
|
||||
{t('login.request_passowrd')}
|
||||
</a>
|
||||
</p>
|
||||
{loginMaxExceeded && (
|
||||
@@ -95,14 +96,14 @@ class AdminLogin extends React.Component {
|
||||
>
|
||||
{this.props.passwordRequestSuccess}{' '}
|
||||
<a className={styles.signInLink} href="#">
|
||||
Sign in
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</p>
|
||||
) : (
|
||||
<form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
@@ -112,17 +113,15 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleRequestPassword}
|
||||
>
|
||||
Reset Password
|
||||
{t('login.reset_password')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>
|
||||
Sign in to interact with your community.
|
||||
</p>
|
||||
<h1 className={styles.loginHeader}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.loginCTA}>{t('login.sign_in_message')}</p>
|
||||
{this.state.requestPassword ? requestPasswordForm : signInForm}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -19,7 +19,9 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const { value: message } = e;
|
||||
const {
|
||||
target: { value: message },
|
||||
} = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
@@ -30,6 +32,12 @@ class BanUserDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const { onCancel, username, info } = this.props;
|
||||
|
||||
@@ -63,7 +71,7 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const { onCancel, onPerform } = this.props;
|
||||
const { onCancel } = this.props;
|
||||
const { message } = this.state;
|
||||
|
||||
return (
|
||||
@@ -95,7 +103,7 @@ class BanUserDialog extends React.Component {
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.send')}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'coral-ui/components/Label';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { t } from 'coral-framework/services/i18n';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
@@ -63,10 +64,14 @@ const CommentLabels = ({
|
||||
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
|
||||
)}
|
||||
{hasSuspectedWords(actions) && (
|
||||
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
|
||||
<FlagLabel iconName="sms_failed">
|
||||
{t('flags.reasons.comment.suspect_word')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
{hasHistoryFlag(actions) && (
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">
|
||||
{t('flags.reasons.comment.trust')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
</div>
|
||||
<Slot
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const CountBadge = ({ count }) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
if (number / 1e9 > 1) {
|
||||
number = `${(number / 1e9).toFixed(1)}${t('modqueue.billion')}`;
|
||||
} else if (number / 1e6 > 1) {
|
||||
number = `${(number / 1e6).toFixed(1)}${t('modqueue.million')}`;
|
||||
} else if (number / 1e3 > 1) {
|
||||
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
|
||||
}
|
||||
number = humanizeNumber(number);
|
||||
|
||||
return <span className={styles.count}>{number}</span>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.external {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.separator h5 {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.slot > * {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './External.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
const External = ({ slot }) => (
|
||||
<IfSlotIsNotEmpty slot={slot}>
|
||||
<div>
|
||||
<div className={styles.external}>
|
||||
<Slot fill={slot} className={styles.slot} />
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h5>Or</h5>
|
||||
</div>
|
||||
</div>
|
||||
</IfSlotIsNotEmpty>
|
||||
);
|
||||
|
||||
External.propTypes = {
|
||||
slot: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default External;
|
||||
@@ -43,21 +43,21 @@ class ForgotPassword extends React.Component {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
{t('login.reset_password')}
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
{t('login.go_back')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
@@ -83,7 +83,7 @@ const CoralHeader = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View latest version
|
||||
{t('configure.view_last_version')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
@@ -92,7 +92,7 @@ const CoralHeader = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Report a bug or give feedback
|
||||
{t('configure.report_bug_or_feedback')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
.karmaTooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 2px 4px 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #0D5B8F;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
|
||||
> i {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
left: -100px;
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.menu::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
line-height: normal;
|
||||
letter-spacing: .4px;
|
||||
min-width: 25px;
|
||||
display: block;
|
||||
|
||||
/* &.reliable { background-color: #03AB61; } */
|
||||
/* &.neutral { background-color: #616161; } */
|
||||
&.unreliable { background-color: #F44336; }
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.strongItem {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.descriptionItem {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #2B7EB5;
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'coral-ui';
|
||||
import styles from './KarmaTooltip.css';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { menuVisible: false };
|
||||
|
||||
class KarmaTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
thresholds: PropTypes.shape({
|
||||
reliable: PropTypes.number.isRequired,
|
||||
unreliable: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
state = initialState;
|
||||
|
||||
toogleMenu = () => {
|
||||
this.setState({ menuVisible: !this.state.menuVisible });
|
||||
};
|
||||
|
||||
hideMenu = () => {
|
||||
this.setState({ menuVisible: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
thresholds: { unreliable },
|
||||
} = this.props;
|
||||
const { menuVisible } = this.state;
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.hideMenu}>
|
||||
<div className={cn(styles.karmaTooltip, 'talk-admin-karma-tooltip')}>
|
||||
<span
|
||||
onClick={this.toogleMenu}
|
||||
className={cn(styles.icon, 'talk-admin-karma-tooltip-icon')}
|
||||
>
|
||||
<Icon name="info" />
|
||||
</span>
|
||||
|
||||
{menuVisible && (
|
||||
<div className={cn(styles.menu, 'talk-admin-karma-tooltip-menu')}>
|
||||
<strong>{t('user_detail.user_karma_score')}</strong>
|
||||
<ul>
|
||||
{/* NOTE: we may display this data in the future, keeping around for that eventuality */}
|
||||
{/* <li>
|
||||
<span>Reliable</span>{' '}
|
||||
<span className={cn(styles.label, styles.reliable)}>
|
||||
≥ {reliable}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Neutral</span>{' '}
|
||||
<span className={cn(styles.label, styles.neutral)}>
|
||||
< {reliable}, > {unreliable}
|
||||
</span>
|
||||
</li> */}
|
||||
<li>
|
||||
<span>{t('user_detail.unreliable')}</span>{' '}
|
||||
<span className={cn(styles.label, styles.unreliable)}>
|
||||
≤ {unreliable}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<a
|
||||
className={styles.link}
|
||||
href={t('user_detail.karma_docs_link')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('user_detail.learn_more')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default KarmaTooltip;
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
@@ -19,8 +20,8 @@ class LoginContainer extends Component {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
<h1 className={styles.header}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.cta}>{t('login.sign_in_message')}</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -14,7 +14,8 @@ const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
className,
|
||||
'talk-admin-reject-button'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
font-size: 24px;
|
||||
line-height: 14px;
|
||||
color: #363636;
|
||||
&:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.radioGroup {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
label.radioGroup {
|
||||
|
||||
&:global(.is-checked) > :global(.mdl-radio__outer-circle),
|
||||
> :global(.mdl-radio__outer-circle) {
|
||||
border-color: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__inner-circle) {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__label) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.perform {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dialog, BareButton } from 'coral-ui';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
import cn from 'classnames';
|
||||
import { RadioGroup, Radio } from 'react-mdl';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import { username as flagReason } from 'coral-framework/graphql/flagReasons';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { reason: flagReason.offensive, message: '' };
|
||||
|
||||
class RejectUsernameDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleReasonChange = event => {
|
||||
this.setState({ reason: event.target.value });
|
||||
};
|
||||
|
||||
handleMessageChange = event => {
|
||||
this.setState({ message: event.target.value });
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
reason: this.state.reason,
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onCancel } = this.props;
|
||||
const { reason, message } = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-reject-username-dialog')}
|
||||
id="rejectUsernameDialog"
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
<div className={styles.close}>
|
||||
<BareButton
|
||||
aria-label="Close"
|
||||
onClick={onCancel}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
×
|
||||
</BareButton>
|
||||
</div>
|
||||
<section className="talk-admin-reject-username-dialog-section">
|
||||
<h1 className={styles.header}>
|
||||
{t('reject_username_dialog.title')}: {this.props.username}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('reject_username_dialog.description')}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('reject_username_dialog.reason')}
|
||||
</legend>
|
||||
<RadioGroup
|
||||
name="reason"
|
||||
value={reason}
|
||||
childContainer="div"
|
||||
onChange={this.handleReasonChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value={flagReason.offensive}>
|
||||
{t('flag_reasons.username.offensive')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.nolike}>
|
||||
{t('flag_reasons.username.nolike')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.impersonating}>
|
||||
{t('flag_reasons.username.impersonating')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.spam}>
|
||||
{t('flag_reasons.username.spam')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.other}>
|
||||
{t('flag_reasons.username.other')}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
{reason === flagReason.other && (
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('reject_username_dialog.message')}
|
||||
</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
cStyle="white"
|
||||
className={styles.cancel}
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('reject_username_dialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={cn(
|
||||
styles.perform,
|
||||
'talk-admin-reject-username-dialog-continue'
|
||||
)}
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('reject_username_dialog.reject_username')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RejectUsernameDialog;
|
||||
@@ -4,6 +4,8 @@ import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
import External from './External';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
@@ -33,48 +35,55 @@ class SignIn extends React.Component {
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
<div className="talk-admin-login-sign-in">
|
||||
<External slot="authExternalAdminSignIn" />
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label={t('login.password')}
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(
|
||||
styles.signInButton,
|
||||
'talk-admin-login-sign-in-button'
|
||||
)}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
{t('login.sign_in')}
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
{/* TODO: translate */}
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
{t('login.request_passowrd')}
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ import styles from './UserDetail.css';
|
||||
import UserHistory from './UserHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
|
||||
import {
|
||||
getReliability,
|
||||
isSuspended,
|
||||
isUsernameRejected,
|
||||
isUsernameChanged,
|
||||
isBanned,
|
||||
getKarma,
|
||||
} from 'coral-framework/utils/user';
|
||||
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import {
|
||||
@@ -25,7 +29,27 @@ 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';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const filterOutLocalProfiles = ({ provider }) => provider !== 'local';
|
||||
|
||||
/**
|
||||
* getUserStatusArray
|
||||
* returns an array of active status(es)
|
||||
* i.e if suspension is active, it returns suspension
|
||||
*/
|
||||
|
||||
function getUserStatusArray(user) {
|
||||
const statusMap = {
|
||||
suspended: isSuspended,
|
||||
banned: isBanned,
|
||||
usernameRejected: isUsernameRejected,
|
||||
usernameChanged: isUsernameChanged,
|
||||
};
|
||||
return Object.keys(statusMap).filter(k => statusMap[k](user));
|
||||
}
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
changeTab = tab => {
|
||||
@@ -44,6 +68,12 @@ class UserDetail extends React.Component {
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
showRejectUsernameDialog = () =>
|
||||
this.props.showRejectUsernameDialog({
|
||||
userId: this.props.root.user.id,
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.hideUserDetail}>
|
||||
@@ -64,22 +94,56 @@ class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const { root: { user } } = this.props;
|
||||
getActionMenuLabel(user) {
|
||||
const userStatusArr = getUserStatusArray(user);
|
||||
const count = userStatusArr.length;
|
||||
|
||||
if (isBanned(user)) {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
if (count > 1) {
|
||||
return `Status (${count})`;
|
||||
} else {
|
||||
const activeStatus = userStatusArr[0];
|
||||
switch (activeStatus) {
|
||||
case 'suspended':
|
||||
return t('user_detail.suspended');
|
||||
case 'banned':
|
||||
return t('user_detail.banned');
|
||||
case 'usernameRejected':
|
||||
return (
|
||||
<span>
|
||||
{t('user_detail.username')}
|
||||
{` `}
|
||||
<Icon name="cancel" />
|
||||
</span>
|
||||
);
|
||||
case 'usernameChanged':
|
||||
return (
|
||||
<span>
|
||||
{t('user_detail.username')}
|
||||
{` `}
|
||||
<Icon name="access_time" />
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return activeStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
goToReportedUsernames = () => {
|
||||
const { router } = this.props;
|
||||
router.push('/admin/community/flagged');
|
||||
};
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
root: {
|
||||
me,
|
||||
user,
|
||||
totalComments,
|
||||
rejectedComments,
|
||||
settings: { karmaThresholds },
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
@@ -97,7 +161,7 @@ class UserDetail extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
let rejectedPercent = rejectedComments / totalComments * 100;
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
rejectedPercent = 0;
|
||||
@@ -105,6 +169,8 @@ class UserDetail extends React.Component {
|
||||
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
const usernameRejected = isUsernameRejected(user);
|
||||
const usernameChanged = isUsernameChanged(user);
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
@@ -137,7 +203,7 @@ class UserDetail extends React.Component {
|
||||
},
|
||||
'talk-admin-user-detail-actions-button'
|
||||
)}
|
||||
label={this.getActionMenuLabel()}
|
||||
label={this.getActionMenuLabel(user)}
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
@@ -164,6 +230,27 @@ class UserDetail extends React.Component {
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{usernameChanged && (
|
||||
<ActionsMenuItem onClick={this.goToReportedUsernames}>
|
||||
{t('user_detail.username_needs_approval')}
|
||||
{` `}
|
||||
<Icon name="launch" />
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{usernameRejected && !usernameChanged ? (
|
||||
<ActionsMenuItem disabled>
|
||||
{t('user_detail.username_rejected')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
disabled={me.id === user.id || usernameChanged}
|
||||
>
|
||||
{t('user_detail.reject_username')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
)}
|
||||
|
||||
@@ -177,7 +264,19 @@ class UserDetail extends React.Component {
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="perm_identity" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.id')}:
|
||||
</span>
|
||||
{user.id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={user.id}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
@@ -185,20 +284,35 @@ class UserDetail extends React.Component {
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{user.email}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={user.email}
|
||||
/>
|
||||
</li>
|
||||
|
||||
{user.profiles
|
||||
.filter(filterOutLocalProfiles)
|
||||
.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul className={styles.stats}>
|
||||
@@ -216,18 +330,21 @@ class UserDetail extends React.Component {
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
styles[getReliability(user.reliable.flagger)]
|
||||
)}
|
||||
>
|
||||
{capitalize(getReliability(user.reliable.flagger))}
|
||||
</span>
|
||||
<li className={cn(styles.stat, styles.karmaStat)}>
|
||||
<div>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.karma')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statKarmaResult,
|
||||
styles[getKarma(user.reliable.commenter)]
|
||||
)}
|
||||
>
|
||||
{humanizeNumber(user.reliable.commenterKarma)}
|
||||
</span>
|
||||
</div>
|
||||
<KarmaTooltip thresholds={karmaThresholds.comment} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -339,6 +456,7 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
UserDetail.propTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
@@ -355,11 +473,13 @@ UserDetail.propTypes = {
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
modal: PropTypes.bool,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserDetail;
|
||||
|
||||
@@ -114,6 +114,7 @@ class UserDetailComment extends React.Component {
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
|
||||
@@ -10,7 +10,10 @@ import ApproveButton from './ApproveButton';
|
||||
const UserDetailCommentList = props => {
|
||||
const {
|
||||
root,
|
||||
root: { user, comments: { nodes, hasNextPage } },
|
||||
root: {
|
||||
user,
|
||||
comments: { nodes, hasNextPage },
|
||||
},
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
selectedCommentIds,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
@@ -12,7 +12,7 @@ import { compose } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
banUser = async () => {
|
||||
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' });
|
||||
@@ -77,7 +77,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withBanUser,
|
||||
withSetCommentStatus
|
||||
)(BanUserDialogContainer);
|
||||
|
||||
@@ -6,6 +6,7 @@ import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
@@ -41,6 +42,7 @@ class LayoutContainer extends React.Component {
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<RejectUsernameDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
@@ -79,4 +81,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutContainer);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import RejectUsernameDialog from '../components/RejectUsernameDialog';
|
||||
import { hideRejectUsernameDialog } from '../actions/rejectUsernameDialog';
|
||||
import {
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { compose } from 'react-apollo';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
|
||||
class RejectUsernameDialogContainer extends Component {
|
||||
rejectUsername = async ({ reason, message }) => {
|
||||
const {
|
||||
postFlag,
|
||||
rejectUsername,
|
||||
hideRejectUsernameDialog,
|
||||
userId,
|
||||
} = this.props;
|
||||
|
||||
// First flag the user.
|
||||
try {
|
||||
await postFlag({
|
||||
item_id: userId,
|
||||
item_type: 'USERS',
|
||||
reason,
|
||||
message,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore already exists error, otherwise show error.
|
||||
if (
|
||||
error.errors &&
|
||||
(error.errors.length !== 1 ||
|
||||
error.errors[0].translation_key !== 'ALREADY_EXISTS')
|
||||
) {
|
||||
notify('error', getErrorMessages(error));
|
||||
}
|
||||
}
|
||||
|
||||
await rejectUsername(userId);
|
||||
hideRejectUsernameDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RejectUsernameDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.rejectUsername}
|
||||
onCancel={this.props.hideRejectUsernameDialog}
|
||||
username={this.props.username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialogContainer.propTypes = {
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
userId: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
rejectUsernameDialog: { open, userId, username },
|
||||
}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideRejectUsernameDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withRejectUsername,
|
||||
withPostFlag({ notifyOnError: false })
|
||||
)(RejectUsernameDialogContainer);
|
||||
@@ -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,7 @@ SignInContainer.propTypes = {
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn)(SignInContainer);
|
||||
export default compose(
|
||||
withSignIn,
|
||||
withPopupAuthHandler
|
||||
)(SignInContainer);
|
||||
|
||||
@@ -86,7 +86,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSuspendUser,
|
||||
withSetCommentStatus,
|
||||
withOrganizationName
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import UserDetail from '../components/UserDetail';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
@@ -21,11 +22,14 @@ import {
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
import { showBanUserDialog } from 'actions/banUserDialog';
|
||||
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
|
||||
import { showRejectUsernameDialog } from 'actions/rejectUsernameDialog';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
|
||||
@@ -131,6 +135,7 @@ class UserDetailContainer extends React.Component {
|
||||
loading={loading}
|
||||
error={this.props.data && this.props.data.error}
|
||||
loadMore={this.loadMore}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
@@ -148,6 +153,7 @@ UserDetailContainer.propTypes = {
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -179,12 +185,14 @@ export const withUserDetailQuery = withQuery(
|
||||
id
|
||||
username
|
||||
created_at
|
||||
email
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
reliable {
|
||||
flagger
|
||||
commenter
|
||||
commenterKarma
|
||||
}
|
||||
state {
|
||||
status {
|
||||
@@ -225,6 +233,14 @@ export const withUserDetailQuery = withQuery(
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
settings {
|
||||
karmaThresholds {
|
||||
comment {
|
||||
reliable
|
||||
unreliable
|
||||
}
|
||||
}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
@@ -265,6 +281,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
{
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
@@ -277,9 +294,15 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withUserDetailQuery,
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser
|
||||
withUnsuspendUser,
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
withRouter
|
||||
)(UserDetailContainer);
|
||||
|
||||
@@ -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 } }) => ({
|
||||
@@ -47,7 +66,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
SuspendUser: ({ variables: { input: { id, until } } }) => ({
|
||||
SuspendUser: ({
|
||||
variables: {
|
||||
input: { id, until },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
|
||||
@@ -73,7 +96,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
UnsuspendUser: ({ variables: { input: { id } } }) => ({
|
||||
UnsuspendUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -98,7 +125,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
BanUser: ({ variables: { input: { id } } }) => ({
|
||||
BanUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -123,7 +154,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
UnbanUser: ({ variables: { input: { id } } }) => ({
|
||||
UnbanUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -156,7 +191,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;
|
||||
@@ -173,6 +210,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
// No need to update, when user was not in the flagged users queue.
|
||||
// TODO: this should be more generic, e.g. looking at the history.
|
||||
if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
@@ -185,41 +228,14 @@ 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;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_ApproveUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
RejectUsername: ({ variables: { id } }) => ({
|
||||
optimisticResponse: {
|
||||
@@ -231,6 +247,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
// No need to update, when user was not in the flagged users queue.
|
||||
// TODO: this should be more generic, e.g. looking at the history.
|
||||
if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
@@ -251,35 +273,6 @@ export default {
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_RejectUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
UpdateSettings: ({ variables: { input } }) => ({
|
||||
updateQueries: {
|
||||
@@ -295,12 +288,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),
|
||||
},
|
||||
|
||||
@@ -5,10 +5,12 @@ import moderation from './moderation';
|
||||
import install from './install';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import rejectUsernameDialog from './rejectUsernameDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
rejectUsernameDialog,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
};
|
||||
|
||||
export default function rejectUsernameDialog(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_REJECT_USERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
};
|
||||
case HIDE_REJECT_USERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,7 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
|
||||
.roleDropdown {
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.roleOption {
|
||||
|
||||
@@ -130,7 +130,9 @@ class People extends React.Component {
|
||||
{user.username}
|
||||
</button>
|
||||
<span className={styles.email}>
|
||||
{user.profiles.map(({ id }) => id)}
|
||||
{user.email
|
||||
? user.email
|
||||
: user.profiles.map(p => p.id).join(', ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
@@ -200,7 +202,7 @@ class People extends React.Component {
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
className={cn(
|
||||
toggleClassName={cn(
|
||||
'talk-admin-community-people-dd-role',
|
||||
styles.roleDropdown
|
||||
)}
|
||||
|
||||
@@ -71,7 +71,7 @@ class RejectUsernameDialog extends Component {
|
||||
<Dialog
|
||||
className={cn(
|
||||
styles.suspendDialog,
|
||||
'talk-admin-reject-username-dialog'
|
||||
'talk-admin-reject-reported-username-dialog'
|
||||
)}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
@@ -85,7 +85,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div
|
||||
className={cn(
|
||||
styles.container,
|
||||
`talk-admin-reject-username-dialog-step-${stage}`
|
||||
`talk-admin-reject-reported-username-dialog-step-${stage}`
|
||||
)}
|
||||
>
|
||||
<div className={styles.description}>
|
||||
@@ -101,7 +101,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-username-dialog-suspension-message')}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-reported-username-dialog-suspension-message')}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div
|
||||
className={cn(
|
||||
styles.modalButtons,
|
||||
'talk-admin-reject-username-dialog-buttons'
|
||||
'talk-admin-reject-reported-username-dialog-buttons'
|
||||
)}
|
||||
>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
@@ -118,7 +118,7 @@ class RejectUsernameDialog extends Component {
|
||||
key={i}
|
||||
className={cn(
|
||||
'talk-admin-username-dialog-button',
|
||||
`talk-admin-reject-username-dialog-button-${key}`
|
||||
`talk-admin-reject-reported-username-dialog-button-${key}`
|
||||
)}
|
||||
onClick={this.onActionClick(stage, i)}
|
||||
>
|
||||
|
||||
@@ -46,7 +46,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameFlagged: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -62,7 +66,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameApproved: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -78,7 +86,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameRejected: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -96,7 +108,9 @@ class FlaggedAccountsContainer extends Component {
|
||||
prev,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameChanged: { previousUsername, user } },
|
||||
data: {
|
||||
usernameChanged: { previousUsername, user },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
@@ -297,7 +311,10 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withApproveUsername,
|
||||
withQuery(
|
||||
gql`
|
||||
|
||||
@@ -15,7 +15,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameFlagged: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -24,7 +28,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameApproved: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -33,7 +41,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameRejected: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -42,7 +54,13 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_CHANGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameChanged: { user } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
usernameChanged: { user },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -98,9 +116,15 @@ const fields = `
|
||||
status {
|
||||
username {
|
||||
status
|
||||
history {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
|
||||
|
||||
@@ -200,7 +200,10 @@ const SEARCH_QUERY = gql`
|
||||
`;
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetUserRole,
|
||||
withUnsuspendUser,
|
||||
withUnbanUser,
|
||||
|
||||
@@ -19,6 +19,9 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withRejectUsername
|
||||
)(RejectUsernameDialog);
|
||||
|
||||
@@ -106,6 +106,23 @@ export function handleFlaggedAccountsChange(root, user, notify) {
|
||||
}
|
||||
}
|
||||
|
||||
export const wasUsernameReported = user => {
|
||||
const previousStatus =
|
||||
user.state.status.username.history[
|
||||
user.state.status.username.history.length - 2
|
||||
];
|
||||
|
||||
// Check for correct previous status
|
||||
if (!['SET', 'CHANGES'].includes(previousStatus)) {
|
||||
return false;
|
||||
}
|
||||
// Check for flags
|
||||
if (user.action_summaries.every(as => as.count === 0)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
@@ -119,7 +136,9 @@ export function handleIndicatorChange(root, user) {
|
||||
return incrementFlaggedUserCount(root);
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return decrementFlaggedUserCount(root);
|
||||
if (wasUsernameReported(user)) {
|
||||
return decrementFlaggedUserCount(root);
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class EmbedLink extends Component {
|
||||
`;
|
||||
|
||||
return (
|
||||
<ConfigureCard title={'Embed Comment Stream'}>
|
||||
<ConfigureCard title={t('configure.embed_comment_stream')}>
|
||||
<p>{t('configure.copy_and_paste')}</p>
|
||||
<textarea
|
||||
rows={5}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SelectField, Option } from 'react-mdl-selectfield';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './StreamSettings.css';
|
||||
import { Textfield } from 'react-mdl';
|
||||
import { Icon, TextArea } from 'coral-ui';
|
||||
import { Icon } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
|
||||
@@ -66,8 +66,8 @@ class StreamSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateClosedMessage = event => {
|
||||
const updater = { closedMessage: { $set: event.target.value } };
|
||||
updateClosedMessage = value => {
|
||||
const updater = { closedMessage: { $set: value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
@@ -82,6 +82,20 @@ class StreamSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDisableCommenting = () => {
|
||||
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 },
|
||||
@@ -164,7 +178,7 @@ class StreamSettings extends React.Component {
|
||||
>
|
||||
<p>{t('configure.closed_comments_desc')}</p>
|
||||
<div>
|
||||
<TextArea
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateClosedMessage}
|
||||
value={settings.closedMessage}
|
||||
@@ -192,6 +206,25 @@ class StreamSettings extends React.Component {
|
||||
|
||||
{t('configure.edit_comment_timeframe_text_post')}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.disableCommenting}
|
||||
onCheckbox={this.updateDisableCommenting}
|
||||
title={t('configure.disable_commenting_title')}
|
||||
>
|
||||
<p>{t('configure.disable_commenting_desc')}</p>
|
||||
<div
|
||||
className={cn(
|
||||
styles.configSettingDisableCommenting,
|
||||
settings.disableCommenting ? null : styles.hidden
|
||||
)}
|
||||
>
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateDisableCommentingMessage}
|
||||
value={settings.disableCommentingMessage}
|
||||
/>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.autoCloseStream}
|
||||
onCheckbox={this.updateAutoClose}
|
||||
|
||||
@@ -174,7 +174,10 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
withMergedSettings('root.settings', 'pending', 'mergedSettings')
|
||||
|
||||
@@ -42,7 +42,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -37,7 +37,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -39,11 +39,16 @@ export default compose(
|
||||
autoCloseStream
|
||||
closedTimeout
|
||||
closedMessage
|
||||
disableCommenting
|
||||
disableCommentingMessage
|
||||
${getSlotFragmentSpreads(slots, 'settings')}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -39,7 +39,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -83,4 +83,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstallContainer);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.root {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
@@ -13,7 +12,6 @@
|
||||
margin-top: 13px;
|
||||
min-height: 0;
|
||||
outline: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
into its own layer.
|
||||
@@ -21,7 +19,6 @@
|
||||
https://www.pivotaltracker.com/story/show/151142211
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -39,7 +36,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.author {
|
||||
@@ -74,7 +70,7 @@
|
||||
max-width: 500px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.created {
|
||||
@@ -95,7 +91,6 @@
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #063b9a;
|
||||
@@ -103,17 +98,15 @@
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,12 +131,10 @@
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
@@ -172,7 +163,6 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 14px;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -181,7 +171,6 @@
|
||||
@media (--big-viewport) {
|
||||
.root {
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
@@ -190,7 +179,6 @@
|
||||
|
||||
.commentContent {
|
||||
display: flex;
|
||||
|
||||
blockquote {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
@@ -170,6 +170,7 @@ class Comment extends React.Component {
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
|
||||
@@ -24,6 +24,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
|
||||
>
|
||||
{items.map(queue => (
|
||||
<Link
|
||||
id={`talk-admin-moderate-tab-${queue.key}`}
|
||||
key={queue.key}
|
||||
to={getModPath(queue.key, asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -380,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 (
|
||||
<div className={styles.root}>
|
||||
@@ -401,7 +407,7 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={rootClassName}>
|
||||
<Comment
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
@@ -423,7 +429,7 @@ class ModerationQueue extends React.Component {
|
||||
const view = this.state.view;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={rootClassName}>
|
||||
<ViewMore
|
||||
viewMore={() => this.viewNewComments()}
|
||||
count={comments.length - view.length}
|
||||
|
||||
@@ -3,9 +3,13 @@ import PropTypes from 'prop-types';
|
||||
import styles from './StorySearch.css';
|
||||
import { Button, Spinner, Icon } from 'coral-ui';
|
||||
import Story from './Story';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const StorySearch = props => {
|
||||
const { root: { assets }, data: { loading } } = props;
|
||||
const {
|
||||
root: { assets },
|
||||
data: { loading },
|
||||
} = props;
|
||||
|
||||
if (!props.moderation.storySearchVisible) {
|
||||
return null;
|
||||
@@ -33,14 +37,14 @@ const StorySearch = props => {
|
||||
onClick={props.search}
|
||||
raised
|
||||
>
|
||||
Search
|
||||
{t('streams.search')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
{props.assetId && (
|
||||
<div className={styles.cta}>
|
||||
<a onClick={props.goToModerateAll}>
|
||||
Moderate comments on All Stories
|
||||
{t('moderate_all_streams')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -48,13 +52,15 @@ const StorySearch = props => {
|
||||
{props.moderation.storySearchString ? (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="search" />
|
||||
<span className={styles.headlineRecent}>Search Results</span>
|
||||
<span className={styles.headlineRecent}>
|
||||
{t('streams.search_results')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="access_time" />
|
||||
<span className={styles.headlineRecent}>
|
||||
Most Recent Stories
|
||||
{t('streams.most_recent_stories')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -82,7 +88,9 @@ const StorySearch = props => {
|
||||
)}
|
||||
|
||||
{assets.nodes.length === 0 && (
|
||||
<div className={styles.noResults}>No results</div>
|
||||
<div className={styles.noResults}>
|
||||
{t('streams.search_results')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -31,7 +35,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentFlagged: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -40,7 +48,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -49,7 +61,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAccepted: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -58,7 +74,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentRejected: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -67,7 +87,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentReset: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
|
||||
@@ -71,7 +71,9 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
const { root: { asset, settings } } = this.props;
|
||||
const {
|
||||
root: { asset, settings },
|
||||
} = this.props;
|
||||
const id = getAssetId(this.props);
|
||||
const tab = getTab(this.props);
|
||||
|
||||
@@ -94,7 +96,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -104,7 +110,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAccepted: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -125,7 +135,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentRejected: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -146,7 +160,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentReset: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -167,7 +185,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -177,7 +199,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentFlagged: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -289,7 +315,11 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { root, root: { asset, settings }, data } = this.props;
|
||||
const {
|
||||
root,
|
||||
root: { asset, settings },
|
||||
data,
|
||||
} = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
if (assetId) {
|
||||
@@ -546,7 +576,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetCommentStatus,
|
||||
withModQueueQuery
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -112,4 +112,7 @@ export const withAssetSearchQuery = withQuery(
|
||||
}
|
||||
);
|
||||
|
||||
export default compose(withRouter, withAssetSearchQuery)(StorySearchContainer);
|
||||
export default compose(
|
||||
withRouter,
|
||||
withAssetSearchQuery
|
||||
)(StorySearchContainer);
|
||||
|
||||
@@ -92,9 +92,6 @@
|
||||
|
||||
.statusDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.statusDropdownOption {
|
||||
min-width: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,20 +22,12 @@ class Stories extends Component {
|
||||
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
|
||||
return (
|
||||
<Dropdown
|
||||
className={styles.statusDropdown}
|
||||
toggleClassName={styles.statusDropdown}
|
||||
value={closed}
|
||||
onChange={value => this.props.onStatusChange(value, id)}
|
||||
>
|
||||
<Option
|
||||
value={false}
|
||||
label={t('streams.open')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option
|
||||
value={true}
|
||||
label={t('streams.closed')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option value={false} label={t('streams.open')} />
|
||||
<Option value={true} label={t('streams.closed')} />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -113,4 +113,7 @@ StoriesContainer.propTypes = {
|
||||
updateAssetState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(StoriesContainer);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { userBanned: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
userBanned: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_account_has_been_banned'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -51,7 +57,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USER_SUSPENDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { userSuspended: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
userSuspended: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_account_has_been_suspended'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -61,7 +73,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { usernameRejected: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
usernameRejected: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_username_has_been_rejected'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -324,7 +342,10 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
withPopupAuthHandler,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery
|
||||
)(EmbedContainer);
|
||||
|
||||
@@ -126,7 +126,9 @@ export default {
|
||||
},
|
||||
mutations: {
|
||||
PostComment: ({
|
||||
variables: { input: { asset_id, body, parent_id, tags = [] } },
|
||||
variables: {
|
||||
input: { asset_id, body, parent_id, tags = [] },
|
||||
},
|
||||
state: { auth },
|
||||
}) => ({
|
||||
optimisticResponse: {
|
||||
@@ -193,7 +195,13 @@ export default {
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{ mutationResult: { data: { createComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
createComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (
|
||||
(![ADMIN, MODERATOR].includes(prev.me.role) &&
|
||||
@@ -208,7 +216,13 @@ export default {
|
||||
},
|
||||
CoralEmbedStream_Profile: (
|
||||
prev,
|
||||
{ mutationResult: { data: { createComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
createComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return update(prev, {
|
||||
me: {
|
||||
@@ -224,7 +238,13 @@ export default {
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{ mutationResult: { data: { editComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
editComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (
|
||||
!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -139,7 +139,10 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSettingsFragments,
|
||||
withUpdateAssetSettings,
|
||||
withMergedSettings('asset.settings', 'pending', 'mergedSettings')
|
||||
|
||||
@@ -22,7 +22,14 @@ class CommentHistoryContainer extends Component {
|
||||
limit: 5,
|
||||
cursor: this.props.root.me.comments.endCursor,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
|
||||
updateQuery: (
|
||||
previous,
|
||||
{
|
||||
fetchMoreResult: {
|
||||
me: { comments },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const updated = update(previous, {
|
||||
me: {
|
||||
comments: {
|
||||
@@ -97,7 +104,10 @@ const mapStateToProps = state => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, null),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
),
|
||||
withCommentHistoryFragments,
|
||||
withFetchMore
|
||||
)(CommentHistoryContainer);
|
||||
|
||||
@@ -80,6 +80,7 @@ const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
export default compose(connect(mapStateToProps), withProfileQuery)(
|
||||
ProfileContainer
|
||||
);
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
withProfileQuery
|
||||
)(ProfileContainer);
|
||||
|
||||
@@ -55,7 +55,10 @@ export default compose(
|
||||
${Settings.fragments.root}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSlotElements({
|
||||
slot: 'profileSettings',
|
||||
propName: 'profileSettingsSlotElements',
|
||||
|
||||
@@ -117,8 +117,12 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const { comment: { replies: prevReplies } } = this.props;
|
||||
const { comment: { replies: nextReplies } } = next;
|
||||
const {
|
||||
comment: { replies: prevReplies },
|
||||
} = this.props;
|
||||
const {
|
||||
comment: { replies: nextReplies },
|
||||
} = next;
|
||||
if (
|
||||
prevReplies &&
|
||||
nextReplies &&
|
||||
@@ -243,7 +247,10 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
loadNewReplies = () => {
|
||||
const { comment: { replies, replyCount, id }, emit } = this.props;
|
||||
const {
|
||||
comment: { replies, replyCount, id },
|
||||
emit,
|
||||
} = this.props;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.setState({ loadingState: 'loading' });
|
||||
this.props
|
||||
@@ -292,7 +299,11 @@ export default class Comment extends React.Component {
|
||||
// getVisibileReplies returns a list containing comments
|
||||
// which were authored by current user or comes before the `idCursor`.
|
||||
getVisibileReplies() {
|
||||
const { comment: { replies }, currentUser, liveUpdates } = this.props;
|
||||
const {
|
||||
comment: { replies },
|
||||
currentUser,
|
||||
liveUpdates,
|
||||
} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
const userId = currentUser ? currentUser.id : null;
|
||||
|
||||
@@ -745,10 +756,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 (
|
||||
<div className={rootClassName} id={id}>
|
||||
{isCommentDeleted(comment) ? (
|
||||
<CommentTombstone action="deleted" />
|
||||
<Slot
|
||||
fill="commentTombstone"
|
||||
defaultComponent={CommentTombstone}
|
||||
size={1}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{this.renderComment()}
|
||||
|
||||
@@ -39,6 +39,7 @@ class CommentTombstone extends React.Component {
|
||||
|
||||
CommentTombstone.propTypes = {
|
||||
action: PropTypes.string,
|
||||
comment: PropTypes.object,
|
||||
onUndo: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const ModerationLink = props =>
|
||||
className="talk-embed-stream-moderation-link"
|
||||
href={`${BASE_PATH}admin/moderate/${props.assetId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import InfoBox from './InfoBox';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import ModerationLink from './ModerationLink';
|
||||
import Markdown from 'coral-framework/components/Markdown';
|
||||
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import CommentBox from '../containers/CommentBox';
|
||||
@@ -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}
|
||||
@@ -208,14 +211,17 @@ class Stream extends React.Component {
|
||||
root,
|
||||
appendItemArray,
|
||||
asset,
|
||||
asset: { comment: highlightedComment, settings: { questionBoxEnable } },
|
||||
asset: {
|
||||
comment: highlightedComment,
|
||||
settings: { questionBoxEnable },
|
||||
},
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
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 +299,13 @@ class Stream extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
<div>
|
||||
{asset.isClosed ? (
|
||||
<Markdown content={asset.settings.closedMessage} />
|
||||
) : (
|
||||
<Markdown content={asset.settings.disableCommentingMessage} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Slot fill="stream" passthrough={slotPassthrough} />
|
||||
|
||||
@@ -24,6 +24,7 @@ const slots = [
|
||||
'commentAuthorName',
|
||||
'commentAuthorTags',
|
||||
'commentTimestamp',
|
||||
'commentTombstone',
|
||||
'commentContent',
|
||||
];
|
||||
|
||||
|
||||
@@ -215,7 +215,10 @@ const mapStateToProps = state => ({
|
||||
|
||||
const enhance = compose(
|
||||
withHooks(['preSubmit', 'postSubmit']),
|
||||
connect(mapStateToProps, null)
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
export default enhance(CommentBox);
|
||||
|
||||
@@ -33,4 +33,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CommentNotFound);
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(CommentNotFound);
|
||||
|
||||
@@ -54,7 +54,11 @@ class StreamContainer extends React.Component {
|
||||
},
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
@@ -87,7 +91,14 @@ class StreamContainer extends React.Component {
|
||||
variables: {
|
||||
assetId: this.props.asset.id,
|
||||
},
|
||||
updateQuery: (prev, { subscriptionData: { data: { commentAdded } } }) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (
|
||||
@@ -434,6 +445,8 @@ const fragments = {
|
||||
questionBoxIcon
|
||||
closedTimeout
|
||||
closedMessage
|
||||
disableCommenting
|
||||
disableCommentingMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
@@ -486,7 +499,10 @@ const mapDispatchToProps = dispatch =>
|
||||
export default compose(
|
||||
withFragments(fragments),
|
||||
withEmit,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withPostComment,
|
||||
// `talk-plugin-flags` has a custom error handling logic.
|
||||
withPostFlag({ notifyOnError: false }),
|
||||
|
||||
@@ -96,7 +96,13 @@ export const logout = () => async (
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
try {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
// We ignore any REST related errors from the delete action, which may/may
|
||||
// not have had a cookie/token attached to it. The logout action was still
|
||||
// called, so we still want to cleanup.
|
||||
}
|
||||
|
||||
// Clear the auth data persisted to localStorage.
|
||||
cleanAuthData(localStorage);
|
||||
|
||||
@@ -78,7 +78,12 @@ function markLinks(body, keyPrefix) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(
|
||||
<a key={`${keyPrefix}_${i}`} href={match.url} target="_blank">
|
||||
<a
|
||||
key={`${keyPrefix}_${i}`}
|
||||
href={match.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{match.text}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -103,5 +103,8 @@ export default compose(
|
||||
size: props => props.size,
|
||||
defaultComponent: props => props.defaultComponent,
|
||||
}),
|
||||
connect(mapStateToProps, null)
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)
|
||||
)(Slot);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -116,7 +116,7 @@ export const withRemoveTag = withMutation(
|
||||
asset_id: assetId,
|
||||
item_type: itemType,
|
||||
},
|
||||
o3timisticResponse: {
|
||||
optimisticResponse: {
|
||||
removeTag: {
|
||||
__typename: 'ModifyTagResponse',
|
||||
errors: null,
|
||||
@@ -242,6 +242,18 @@ export const withUnsuspendUser = withMutation(
|
||||
}
|
||||
);
|
||||
|
||||
const SetUsernameStatusFragment = gql`
|
||||
fragment Talk_SetUsernameStatus on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const withApproveUsername = withMutation(
|
||||
gql`
|
||||
mutation ApproveUsername($id: ID!) {
|
||||
@@ -257,6 +269,27 @@ export const withApproveUsername = withMutation(
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({
|
||||
fragment: SetUsernameStatusFragment,
|
||||
id: fragmentId,
|
||||
data,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
@@ -278,6 +311,27 @@ export const withRejectUsername = withMutation(
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({
|
||||
fragment: SetUsernameStatusFragment,
|
||||
id: fragmentId,
|
||||
data,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const DECIMAL_AMOUNT = 1;
|
||||
|
||||
export function humanizeNumber(number) {
|
||||
const abs = Math.abs(number);
|
||||
|
||||
if (abs >= 1e9) {
|
||||
number = `${(number / 1e9).toFixed(DECIMAL_AMOUNT)}${t(
|
||||
'modqueue.billion'
|
||||
)}`;
|
||||
} else if (abs >= 1e6) {
|
||||
number = `${(number / 1e6).toFixed(DECIMAL_AMOUNT)}${t(
|
||||
'modqueue.million'
|
||||
)}`;
|
||||
} else if (abs >= 1e3) {
|
||||
number = `${(number / 1e3).toFixed(DECIMAL_AMOUNT)}${t(
|
||||
'modqueue.thousand'
|
||||
)}`;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
+12
-2
@@ -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;
|
||||
@@ -2,5 +2,8 @@ import { connect } from 'react-redux';
|
||||
|
||||
export default (mapStateToProps, ...rest) => BaseComponent => {
|
||||
BaseComponent.mapStateToProps = mapStateToProps;
|
||||
return connect(mapStateToProps, ...rest)(BaseComponent);
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
...rest
|
||||
)(BaseComponent);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,10 @@ const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ updateUsername, updateStatus }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetUsernameMutation,
|
||||
withSetUsername
|
||||
);
|
||||
|
||||
@@ -121,4 +121,7 @@ const withSignUp = hoistStatics(WrappedComponent => {
|
||||
return WithSignUp;
|
||||
});
|
||||
|
||||
export default compose(withSettingsQuery, withSignUp);
|
||||
export default compose(
|
||||
withSettingsQuery,
|
||||
withSignUp
|
||||
);
|
||||
|
||||
@@ -198,5 +198,11 @@ const mapStateToProps = state => ({
|
||||
* })(MyComponent);
|
||||
*/
|
||||
export default settings => {
|
||||
return compose(connect(mapStateToProps, null), createHOC(settings));
|
||||
return compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
),
|
||||
createHOC(settings)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* createNotificationService returns a notification services based on pym.
|
||||
* @param {func} condition callback that checks that the given argument is valid.
|
||||
* @param {error} string error that is displayed when validation fails.
|
||||
* @return {func} validator function
|
||||
*/
|
||||
export function createValidator(condition, error) {
|
||||
return (v, values) => (condition(v, values) ? undefined : error);
|
||||
}
|
||||
|
||||
/**
|
||||
* composeValidators chains validators and runs them in sequence until
|
||||
* one validator fails and returns an error message.
|
||||
* @param {func[]} validators array of validator functions.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export function composeValidators(...validators) {
|
||||
return value =>
|
||||
validators.reduce(
|
||||
(error, validator) => error || validator(value),
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* required checks that the value is truthy.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const required = createValidator(v => !!v, t('validators.required'));
|
||||
|
||||
/**
|
||||
* Confirm will check that this field has the same value as another.
|
||||
* @param {string} key key of other form field to compare with.
|
||||
* @param {error} string error that is displayed when validation fails.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const confirm = (key, error) =>
|
||||
createValidator((v, data) => v === data[key], error);
|
||||
|
||||
/**
|
||||
* Verify email.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyEmail = createValidator(
|
||||
email => /^.+@.+\..+$/.test(email),
|
||||
t('validators.verify_email')
|
||||
);
|
||||
|
||||
/**
|
||||
* Confirm email.
|
||||
* @param {string} key key of other form field to compare with.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const confirmEmail = key => confirm(key, t('validators.confirm_email'));
|
||||
|
||||
/**
|
||||
* Verify password.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyPassword = createValidator(
|
||||
pass => /^(?=.{8,}).*$/.test(pass),
|
||||
t('validators.verify_password')
|
||||
);
|
||||
|
||||
/**
|
||||
* Confirm password.
|
||||
* @param {string} key key of other form field to compare with.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const confirmPassword = key =>
|
||||
confirm(key, t('validators.confirm_password'));
|
||||
|
||||
/**
|
||||
* Verify username.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyUsername = createValidator(
|
||||
username => /^[a-zA-Z0-9_]+$/.test(username),
|
||||
t('validators.verify_username')
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify organization name.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyOrganizationName = createValidator(
|
||||
org => /^[a-zA-Z0-9_ ]+$/.test(org),
|
||||
t('validators.verify_organization_name')
|
||||
);
|
||||
@@ -9,7 +9,11 @@ export function createReduxEmitter(eventEmitter) {
|
||||
// Handle apollo actions.
|
||||
if (action.type.startsWith('APOLLO_')) {
|
||||
if (action.type === 'APOLLO_SUBSCRIPTION_RESULT') {
|
||||
const { operationName, variables, result: { data } } = action;
|
||||
const {
|
||||
operationName,
|
||||
variables,
|
||||
result: { data },
|
||||
} = action;
|
||||
eventEmitter.emit(`subscription.${operationName}.data`, {
|
||||
variables,
|
||||
data,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user