mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2a9a42ec6 | ||
|
|
b65c07a8a2 | ||
|
|
5984f57682 | ||
|
|
1c844061f4 | ||
|
|
bc878051f8 | ||
|
|
b805451d37 | ||
|
|
4a5424b81d | ||
|
|
18a767b8a4 | ||
|
|
32b6627144 | ||
|
|
e1b3eb9ff7 | ||
|
|
b76578b866 | ||
|
|
c09d1524a9 | ||
|
|
548b56be33 | ||
|
|
ea030af440 | ||
|
|
af59a2e836 | ||
|
|
2ab2b41b55 | ||
|
|
4bb64231e9 | ||
|
|
2800458519 | ||
|
|
90bc9be1b2 | ||
|
|
64a4bf6760 | ||
|
|
6d5b8e73d4 | ||
|
|
87c14dd824 | ||
|
|
e89a15f2d2 | ||
|
|
7d84cff0f7 | ||
|
|
6fa74f84f1 | ||
|
|
10ed5c9a4a | ||
|
|
40b4c0cab4 | ||
|
|
bb544a92b3 | ||
|
|
010f0cb0d7 | ||
|
|
60dd6a37d7 | ||
|
|
455a034802 | ||
|
|
46e707ba51 | ||
|
|
e7e66f1e6f | ||
|
|
8bb38c5ee5 | ||
|
|
c41b3c27ba | ||
|
|
cfd55907bd | ||
|
|
91907e7647 | ||
|
|
a5048eadb4 | ||
|
|
bde08ea256 | ||
|
|
3de6ba1b24 | ||
|
|
dac9401c44 | ||
|
|
bba4474858 | ||
|
|
259f98a4ec | ||
|
|
f7a8f9bbd1 | ||
|
|
83096dbcce | ||
|
|
555a71fd9b | ||
|
|
2c57ec3c7c | ||
|
|
c0e9e4272b | ||
|
|
a31810ddf4 | ||
|
|
f6b2a25e72 | ||
|
|
bd7443672a | ||
|
|
358ea0d37c | ||
|
|
c0dcfe5f4e | ||
|
|
91b7cd10f8 | ||
|
|
f537b4a772 | ||
|
|
a9cefd8cbd | ||
|
|
f08c2f4152 | ||
|
|
cdcd543fe0 | ||
|
|
66aec74b38 | ||
|
|
85dcc8d0ec | ||
|
|
9d572f6c3b | ||
|
|
012ff926eb | ||
|
|
91fd8ceabd | ||
|
|
4a354f0b9a | ||
|
|
3beb62f837 | ||
|
|
57ecba6f33 | ||
|
|
712a53db24 | ||
|
|
f709057933 | ||
|
|
ac89ddc43f | ||
|
|
b0156fa11f | ||
|
|
16eb93792b | ||
|
|
1b1f2a3608 | ||
|
|
6628e6d08d | ||
|
|
e2f94aa141 | ||
|
|
f20b9819aa | ||
|
|
91fa7b2d8d | ||
|
|
74fff2f1ac | ||
|
|
4b063607ab | ||
|
|
9b26bc9d96 | ||
|
|
38eaff5397 | ||
|
|
420513804c | ||
|
|
b0079c191b | ||
|
|
9815248b8d | ||
|
|
79346738d7 | ||
|
|
e16553419d | ||
|
|
dccf182591 | ||
|
|
33057e197f | ||
|
|
605e759bc6 | ||
|
|
706a12d2e2 | ||
|
|
adeca927a6 | ||
|
|
fc14e9d2fd | ||
|
|
49420e152e | ||
|
|
644c291d7f | ||
|
|
40d7500a75 | ||
|
|
3d27c4538a | ||
|
|
942ea7b916 | ||
|
|
4d53a72767 | ||
|
|
23fcba4ccc | ||
|
|
bee4b4454c | ||
|
|
679c6ce335 | ||
|
|
71d2d0ab10 | ||
|
|
d97fb0a83f | ||
|
|
14311dc298 | ||
|
|
7faa00c463 | ||
|
|
d23d426567 | ||
|
|
fe8f3ae4bb | ||
|
|
646b5be8c8 | ||
|
|
ea12c07cd4 | ||
|
|
bb4cd91b0c | ||
|
|
dca759a5e0 | ||
|
|
018b35e42a | ||
|
|
672e36c66c | ||
|
|
b3db231f09 | ||
|
|
38a53d61f3 | ||
|
|
fc15bcae4e | ||
|
|
6959699ecc | ||
|
|
99bb4b7198 | ||
|
|
12a5654fb8 | ||
|
|
dad6534011 | ||
|
|
c36a0bc9e9 | ||
|
|
30a385e03b | ||
|
|
174088fc62 | ||
|
|
16ef833fd8 | ||
|
|
523f23269b | ||
|
|
8dbc17c8d9 | ||
|
|
b8967dcdb4 | ||
|
|
70276c3358 | ||
|
|
1f2eca300a | ||
|
|
3c99b8e7fb | ||
|
|
073fccebd3 | ||
|
|
13e311de7f | ||
|
|
ba81399707 | ||
|
|
cf93727f73 | ||
|
|
fbbd0fa027 | ||
|
|
332679a7bc | ||
|
|
2a2ca86263 | ||
|
|
393ad8530a | ||
|
|
0cd3921f44 | ||
|
|
f954d2be90 | ||
|
|
2a28306ab2 | ||
|
|
e1bc1476ea | ||
|
|
853c1e407e | ||
|
|
77fddba841 | ||
|
|
17d36c69d0 | ||
|
|
1cf6161b06 | ||
|
|
9ef7544788 | ||
|
|
dfccf962d4 | ||
|
|
6c5d94ec3b | ||
|
|
e7eb300e12 | ||
|
|
614f31d0da | ||
|
|
61591a9716 | ||
|
|
367c570747 | ||
|
|
991bdc6089 | ||
|
|
4ce38d429e | ||
|
|
dcee2a4438 | ||
|
|
4403decbe9 | ||
|
|
e30d5a9c62 | ||
|
|
fe94a5a6d1 | ||
|
|
c7b58957cb | ||
|
|
89cf13b4cf | ||
|
|
f0b6679141 | ||
|
|
26253255ce | ||
|
|
64174b97af | ||
|
|
fe0c581ea9 | ||
|
|
4982d8a9ad | ||
|
|
fd9b14a560 | ||
|
|
6139828844 | ||
|
|
c14c3fbe23 | ||
|
|
f34d546a37 | ||
|
|
6da0028258 | ||
|
|
74fbadcc0f | ||
|
|
30ca7b3d64 | ||
|
|
9573c9a7af | ||
|
|
1dec8bfaab | ||
|
|
2625b372e1 | ||
|
|
5f450adbaf | ||
|
|
d386316f8a | ||
|
|
5604aa8525 | ||
|
|
b518358ea8 | ||
|
|
fe74f4a8d2 | ||
|
|
b78d4f29ea | ||
|
|
ca08efea8a | ||
|
|
5718ba6748 | ||
|
|
0123736275 | ||
|
|
18061e3e1c | ||
|
|
b40df9f10c | ||
|
|
0c7f9cdb1e | ||
|
|
ecf63ffb55 | ||
|
|
ba659b47be | ||
|
|
f73f8d916e | ||
|
|
93c8b115f5 | ||
|
|
685695bf43 | ||
|
|
d7c6d7dfac | ||
|
|
c7cfd80c7f | ||
|
|
928e19ed67 | ||
|
|
5a2ce0f6a7 | ||
|
|
312103098c | ||
|
|
21f95c33c2 | ||
|
|
cbcb84f7ac | ||
|
|
303b3ea0f5 | ||
|
|
5ecdfe94cc | ||
|
|
648b46e5af | ||
|
|
e160dfaa3e | ||
|
|
3658a804b0 | ||
|
|
c902a4b05b | ||
|
|
8a3833ec11 | ||
|
|
d45c396013 | ||
|
|
c7d5ab935f | ||
|
|
aa289ecbfe | ||
|
|
de665b3a0d | ||
|
|
06da4d7d26 | ||
|
|
9818b851db |
+2
-1
@@ -53,7 +53,8 @@
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "next"
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
|
||||
@@ -7,7 +7,17 @@ endpoint when the server is running with built assets.
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md)!
|
||||
See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
### General
|
||||
|
||||
See our [Talk Documentation & Guides](https://coralproject.github.io/talk/index.html).
|
||||
|
||||
### Plugins
|
||||
|
||||
See our guide to using and building [Talk Plugins](https://github.com/coralproject/talk/blob/master/PLUGINS.md).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -40,6 +50,7 @@ sign and verify tokens via a `HS256` algorithm.
|
||||
- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
|
||||
- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
|
||||
- `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment
|
||||
- `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`)
|
||||
|
||||
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
|
||||
alternative methods of loading configuration during development.
|
||||
|
||||
@@ -12,6 +12,7 @@ program
|
||||
.command('assets', 'interact with assets')
|
||||
.command('setup', 'setup the application')
|
||||
.command('jobs', 'work with the job queues')
|
||||
.command('token', 'work with the access tokens')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command('plugins', 'provides utilities for interacting with the plugin system')
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function listTokens(userID) {
|
||||
try {
|
||||
let tokens = await TokensService.list(userID);
|
||||
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Name',
|
||||
'Status'
|
||||
]
|
||||
});
|
||||
|
||||
tokens.forEach((token) => {
|
||||
table.push([
|
||||
token.id,
|
||||
token.name,
|
||||
token.active ? 'Active' : 'Revoked'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeToken(tokenID) {
|
||||
try {
|
||||
|
||||
await TokensService.revoke(null, tokenID);
|
||||
|
||||
console.log(`Revoked Token[${tokenID}]`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
|
||||
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('list <userID>')
|
||||
.description('list tokens for a user')
|
||||
.action(listTokens);
|
||||
|
||||
program
|
||||
.command('revoke <tokenID>')
|
||||
.description('revokes a token with a given id')
|
||||
.action(revokeToken);
|
||||
|
||||
program
|
||||
.command('create <userID> <tokenName>')
|
||||
.description('create a token for a user with a given name')
|
||||
.action(createToken);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
+7
-6
@@ -8,6 +8,7 @@ const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
@@ -36,7 +37,7 @@ function getUserCreateAnswers(options) {
|
||||
roles: []
|
||||
};
|
||||
|
||||
if (options.role && UserModel.USER_ROLES.indexOf(options.role) > -1) {
|
||||
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
|
||||
user.roles = [options.role];
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ function getUserCreateAnswers(options) {
|
||||
name: 'roles',
|
||||
message: 'User Role',
|
||||
type: 'checkbox',
|
||||
choices: UserModel.USER_ROLES
|
||||
choices: USER_ROLES
|
||||
}
|
||||
]);
|
||||
}
|
||||
@@ -291,8 +292,8 @@ function mergeUsers(dstUserID, srcUserID) {
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
|
||||
if (UserModel.USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${UserModel.USER_ROLES.join(', ')}.`);
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
@@ -316,8 +317,8 @@ function addRole(userID, role) {
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
|
||||
if (UserModel.USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${UserModel.USER_ROLES.join(', ')}.`);
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,5 +52,6 @@ deployment:
|
||||
|
||||
latest:
|
||||
branch: master
|
||||
owner: coralproject
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, browserHistory} from 'react-router';
|
||||
import {Router, Route, IndexRedirect, browserHistory, Redirect} from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
@@ -49,8 +49,8 @@ const routes = (
|
||||
<Route path='flagged' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path=':id' components={Moderation} />
|
||||
<IndexRedirect to='premod' />
|
||||
<Redirect from=':id' to='all/:id' />
|
||||
<IndexRedirect to='all' />
|
||||
</Route>
|
||||
</Route>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
@@ -19,12 +20,16 @@ import coralApi from '../../../coral-framework/helpers/request';
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
}))
|
||||
.catch((error) => dispatch({type: FETCH_ASSETS_FAILURE, error}));
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
@@ -32,9 +37,12 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
|
||||
export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() =>
|
||||
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch((error) => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
||||
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as actions from '../constants/auth';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as Storage from 'coral-framework/helpers/storage';
|
||||
import {handleAuthToken} from 'coral-framework/actions/auth';
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
@@ -36,16 +38,31 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password')
|
||||
});
|
||||
}
|
||||
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: error.translation_key
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -54,25 +71,30 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPassowordRequest = () => ({
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
|
||||
});
|
||||
|
||||
const forgotPassowordSuccess = () => ({
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPassowordFailure = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch((error) => dispatch(forgotPassowordFailure(error)));
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -105,10 +127,12 @@ export const checkLogin = () => (dispatch) => {
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
|
||||
|
||||
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
|
||||
|
||||
@@ -10,11 +10,12 @@ import {
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_SUSPENDUSER_DIALOG,
|
||||
HIDE_SUSPENDUSER_DIALOG
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
|
||||
@@ -30,7 +31,11 @@ export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
totalPages
|
||||
});
|
||||
})
|
||||
.catch((error) => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchAccounts = () => ({
|
||||
@@ -47,7 +52,6 @@ export const newPage = () => ({
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch) => {
|
||||
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
@@ -65,6 +69,6 @@ export const setCommenterStatus = (id, status) => (dispatch) => {
|
||||
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
|
||||
|
||||
// Suspend User Dialog
|
||||
export const showSuspendUserDialog = (user) => ({type: SHOW_SUSPENDUSER_DIALOG, user});
|
||||
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPENDUSER_DIALOG});
|
||||
// Reject Username Dialog
|
||||
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
|
||||
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
|
||||
|
||||
@@ -2,6 +2,7 @@ import coralApi from 'coral-framework/helpers/request';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const nextStep = () => ({type: actions.NEXT_STEP});
|
||||
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
|
||||
@@ -17,7 +18,8 @@ const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
if (!(formData != null)) {
|
||||
return dispatch(hasError());
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
@@ -40,7 +42,8 @@ const validation = (formData, dispatch, next) => {
|
||||
});
|
||||
|
||||
if (empty.length) {
|
||||
return dispatch(hasError());
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
@@ -59,7 +62,8 @@ const validation = (formData, dispatch, next) => {
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
return dispatch(hasError());
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(clearErrors());
|
||||
@@ -90,7 +94,8 @@ export const finishInstall = () => (dispatch, getState) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(installFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -113,6 +118,7 @@ export const checkInstall = (next) => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkInstallFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,16 +3,6 @@ import * as actions from 'constants/moderation';
|
||||
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
|
||||
export const hideBanUserDialog = () => ({type: actions.HIDE_BANUSER_DIALOG});
|
||||
|
||||
// Suspend User Dialog
|
||||
export const showSuspendUserDialog = (userId, username, commentId, commentStatus) =>
|
||||
({type: actions.SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({type: actions.HIDE_SUSPEND_USER_DIALOG});
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => {
|
||||
try {
|
||||
@@ -51,3 +41,16 @@ export const toggleSelectCommentInUserDetail = (id, active) => {
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleStorySearch = (active) => ({
|
||||
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
|
||||
});
|
||||
|
||||
export const storySearchChange = (value) => ({
|
||||
type: actions.STORY_SEARCH_CHANGE_VALUE,
|
||||
value
|
||||
});
|
||||
|
||||
export const clearState = () => ({
|
||||
type: actions.MODERATION_CLEAR_STATE
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
@@ -20,7 +21,9 @@ export const fetchSettings = () => (dispatch) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error});
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,6 +52,8 @@ export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error});
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
|
||||
|
||||
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to users
|
||||
@@ -10,7 +11,11 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch((error) => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +23,11 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch((error) => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +35,10 @@ export const sendNotificationEmail = (userId, subject, body) => {
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch((error) => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, {PropTypes} from 'react';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
@@ -35,7 +34,7 @@ class AdminLogin extends React.Component {
|
||||
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
|
||||
const signInForm = (
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{t(`error.${errorMessage}`)}</Alert>}
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={onCancel}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
info: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
@@ -16,8 +16,8 @@ const shortcuts = [
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
't': 'modqueue.approve',
|
||||
'r': 'modqueue.reject'
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -40,8 +40,8 @@ export default class ModerationKeysModal extends React.Component {
|
||||
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
|
||||
<p><strong>{t('modqueue.try_these')}:</strong></p>
|
||||
<ul>
|
||||
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>t</span></li>
|
||||
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>r</span></li>
|
||||
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
|
||||
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
|
||||
</ul>
|
||||
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,8 @@ import SuspendUserModal from './SuspendUserModal';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const menuOptionsMap = {
|
||||
'reject': {status: 'REJECTED', icon: 'close', key: 'r'},
|
||||
'approve': {status: 'ACCEPTED', icon: 'done', key: 't'},
|
||||
'reject': {status: 'REJECTED', icon: 'close', key: 'f'},
|
||||
'approve': {status: 'ACCEPTED', icon: 'done', key: 'd'},
|
||||
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'ban': {status: 'BANNED', icon: 'not interested'}
|
||||
};
|
||||
|
||||
+2
-4
@@ -49,7 +49,6 @@ class SuspendUserDialog extends React.Component {
|
||||
handlePerform = () => {
|
||||
|
||||
this.props.onPerform({
|
||||
id: this.props.userId,
|
||||
message: this.state.message,
|
||||
|
||||
// Add 1 minute more to help `timeago.js` to display the correct duration.
|
||||
@@ -153,11 +152,10 @@ class SuspendUserDialog extends React.Component {
|
||||
|
||||
SuspendUserDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
userId: PropTypes.string,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
organizationName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SuspendUserDialog;
|
||||
@@ -133,7 +133,7 @@
|
||||
animation-fill-mode: both; }
|
||||
|
||||
.toastify {
|
||||
z-index: 999;
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
padding: 4px;
|
||||
width: 350px;
|
||||
@@ -197,7 +197,7 @@
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); }
|
||||
.toastify-content--info {
|
||||
background: #2488cb; }
|
||||
background: #404040; }
|
||||
.toastify-content--success {
|
||||
background: #008577; }
|
||||
.toastify-content--warning {
|
||||
@@ -207,6 +207,7 @@
|
||||
|
||||
.toastify__body {
|
||||
color: white;
|
||||
overflow-x: scroll;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rightPanel ul {
|
||||
.rightPanel > ul {
|
||||
list-style: none;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.rightPanel li {
|
||||
.rightPanel > ul > li {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-left: 15px;
|
||||
@@ -54,6 +54,12 @@
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.rightPanel a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.rightPanel .settings {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
@@ -71,12 +71,24 @@ const CoralHeader = ({
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>{t('configure.sign_out')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.layout {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const Layout = ({
|
||||
toggleShortcutModal,
|
||||
restricted = false,
|
||||
...props}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SHOW_BAN_USER_DIALOG = 'SHOW_BAN_USER_DIALOG';
|
||||
export const HIDE_BAN_USER_DIALOG = 'HIDE_BAN_USER_DIALOG';
|
||||
@@ -13,5 +13,5 @@ export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILUR
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
|
||||
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
|
||||
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
|
||||
export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG';
|
||||
export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
|
||||
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
|
||||
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
|
||||
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
|
||||
@@ -12,3 +8,7 @@ export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
|
||||
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
|
||||
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
|
||||
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
|
||||
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
|
||||
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
|
||||
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
|
||||
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG';
|
||||
export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import BanUserDialog from '../components/BanUserDialog';
|
||||
import {hideBanUserDialog} from '../actions/banUserDialog';
|
||||
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {compose} from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
|
||||
banUser = async () => {
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog} = this.props;
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
let note = t('bandialog.note_ban_user');
|
||||
if (this.props.commentStatus && this.props.commentStatus !== 'REJECTED') {
|
||||
note = t('bandialog.note_reject_comment');
|
||||
}
|
||||
return t('bandialog.note', note);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BanUserDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.banUser}
|
||||
onCancel={this.props.hideBanUserDialog}
|
||||
username={this.props.username}
|
||||
info={this.getInfo()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideBanUserDialog,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withSetUserStatus,
|
||||
withSetCommentStatus,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
),
|
||||
)(BanUserDialogContainer);
|
||||
@@ -5,6 +5,8 @@ import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import {logout} from 'coral-framework/actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
@@ -52,7 +54,11 @@ class LayoutContainer extends Component {
|
||||
handleLogout={handleLogout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
/>
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
{this.props.children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import SuspendUserDialog from '../components/SuspendUserDialog';
|
||||
import {hideSuspendUserDialog} from '../actions/suspendUserDialog';
|
||||
import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import get from 'lodash/get';
|
||||
|
||||
class SuspendUserDialogContainer extends Component {
|
||||
|
||||
suspendUser = async ({message, until}) => {
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await suspendUser({id: userId, message, until});
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
notification.success(
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until)),
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
return setCommentStatus({commentId, status: 'REJECTED'})
|
||||
.then((result) => {
|
||||
if (result.data.setCommentStatus.errors) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SuspendUserDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.suspendUser}
|
||||
onCancel={this.props.hideSuspendUserDialog}
|
||||
organizationName={get(this.props, 'root.settings.organizationName')}
|
||||
username={this.props.username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const withOrganizationName = withQuery(gql`
|
||||
query CoralAdmin_SuspendUserDialog {
|
||||
settings {
|
||||
organizationName
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideSuspendUserDialog,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
),
|
||||
withSuspendUser,
|
||||
withSetCommentStatus,
|
||||
withOrganizationName,
|
||||
)(SuspendUserDialogContainer);
|
||||
@@ -1,6 +1,4 @@
|
||||
import {add} from 'coral-framework/services/graphqlRegistry';
|
||||
import update from 'immutability-helper';
|
||||
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
|
||||
|
||||
const extension = {
|
||||
mutations: {
|
||||
@@ -10,58 +8,6 @@ const extension = {
|
||||
RejectUsername: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
}),
|
||||
SetCommentStatus: ({variables: {commentId, status}}) => ({
|
||||
updateQueries: {
|
||||
CoralAdmin_Moderation: (prev) => {
|
||||
const comment = queues.reduce((comment, queue) => {
|
||||
return comment ? comment : prev[queue].nodes.find((c) => c.id === commentId);
|
||||
}, null);
|
||||
|
||||
let acceptedNodes = prev.accepted.nodes;
|
||||
let acceptedCount = prev.acceptedCount;
|
||||
let rejectedNodes = prev.rejected.nodes;
|
||||
let rejectedCount = prev.rejectedCount;
|
||||
|
||||
if (status !== comment.status) {
|
||||
if (status === 'ACCEPTED') {
|
||||
comment.status = 'ACCEPTED';
|
||||
acceptedCount++;
|
||||
acceptedNodes = [comment, ...acceptedNodes];
|
||||
}
|
||||
else if (status === 'REJECTED') {
|
||||
comment.status = 'REJECTED';
|
||||
rejectedCount++;
|
||||
rejectedNodes = [comment, ...rejectedNodes];
|
||||
}
|
||||
}
|
||||
|
||||
const premodNodes = prev.premod.nodes.filter((c) => c.id !== commentId);
|
||||
const flaggedNodes = prev.flagged.nodes.filter((c) => c.id !== commentId);
|
||||
const premodCount = premodNodes.length < prev.premod.nodes.length ? prev.premodCount - 1 : prev.premodCount;
|
||||
const flaggedCount = flaggedNodes.length < prev.flagged.nodes.length ? prev.flaggedCount - 1 : prev.flaggedCount;
|
||||
|
||||
if (status === 'REJECTED') {
|
||||
acceptedNodes = prev.accepted.nodes.filter((c) => c.id !== commentId);
|
||||
acceptedCount = acceptedNodes.length < prev.accepted.nodes.length ? prev.acceptedCount - 1 : prev.acceptedCount;
|
||||
}
|
||||
else if (status === 'ACCEPTED') {
|
||||
rejectedNodes = prev.rejected.nodes.filter((c) => c.id !== commentId);
|
||||
rejectedCount = rejectedNodes.length < prev.rejected.nodes.length ? prev.rejectedCount - 1 : prev.rejectedCount;
|
||||
}
|
||||
|
||||
return update(prev, {
|
||||
premodCount: {$set: Math.max(0, premodCount)},
|
||||
flaggedCount: {$set: Math.max(0, flaggedCount)},
|
||||
acceptedCount: {$set: Math.max(0, acceptedCount)},
|
||||
rejectedCount: {$set: Math.max(0, rejectedCount)},
|
||||
premod: {nodes: {$set: premodNodes}},
|
||||
flagged: {nodes: {$set: flaggedNodes}},
|
||||
accepted: {nodes: {$set: acceptedNodes}},
|
||||
rejected: {nodes: {$set: rejectedNodes}},
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import update from 'immutability-helper';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
|
||||
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
|
||||
const limit = 10;
|
||||
|
||||
const ascending = (a, b) => {
|
||||
const dateA = new Date(a.created_at);
|
||||
const dateB = new Date(b.created_at);
|
||||
if (dateA < dateB) { return -1; }
|
||||
if (dateA > dateB) { return 1; }
|
||||
return 0;
|
||||
};
|
||||
|
||||
const descending = (a, b) => -ascending(a, b);
|
||||
|
||||
function queueHasComment(root, queue, id) {
|
||||
return root[queue].nodes.find((c) => c.id === id);
|
||||
}
|
||||
|
||||
function removeCommentFromQueue(root, queue, id) {
|
||||
if (!queueHasComment(root, queue, id)) {
|
||||
return root;
|
||||
}
|
||||
return update(root, {
|
||||
[`${queue}Count`]: {$set: root[`${queue}Count`] - 1},
|
||||
[queue]: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((c) => c.id !== id)},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function shouldCommentBeAdded(root, queue, comment, sort) {
|
||||
if (root[`${queue}Count`] < limit) {
|
||||
|
||||
// Adding all comments until first limit has reached.
|
||||
return true;
|
||||
}
|
||||
const cursor = new Date(root[queue].endCursor);
|
||||
return sort === 'CHRONOLOGICAL'
|
||||
? new Date(comment.created_at) <= cursor
|
||||
: new Date(comment.created_at) >= cursor;
|
||||
}
|
||||
|
||||
function addCommentToQueue(root, queue, comment, sort) {
|
||||
if (queueHasComment(root, queue, comment.id)) {
|
||||
return root;
|
||||
}
|
||||
|
||||
const sortAlgo = sort === 'CHRONOLOGICAL' ? ascending : descending;
|
||||
const changes = {
|
||||
[`${queue}Count`]: {$set: root[`${queue}Count`] + 1},
|
||||
};
|
||||
|
||||
if (shouldCommentBeAdded(root, queue, comment, sort)) {
|
||||
const nodes = root[queue].nodes.concat(comment).sort(sortAlgo);
|
||||
changes[queue] = {
|
||||
nodes: {$set: nodes},
|
||||
startCursor: {$set: nodes[0].created_at},
|
||||
endCursor: {$set: nodes[nodes.length - 1].created_at},
|
||||
};
|
||||
}
|
||||
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
function getCommentQueues(comment) {
|
||||
const queues = ['all'];
|
||||
if (comment.status === 'ACCEPTED') {
|
||||
queues.push('accepted');
|
||||
}
|
||||
else if (comment.status === 'REJECTED') {
|
||||
queues.push('rejected');
|
||||
}
|
||||
else if (comment.status === 'PREMOD') {
|
||||
queues.push('premod');
|
||||
}
|
||||
if (
|
||||
['NONE', 'PREMOD'].indexOf(comment.status) >= 0
|
||||
&& comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')
|
||||
) {
|
||||
queues.push('flagged');
|
||||
}
|
||||
return queues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assimilate comment changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @param {string} sort current sort order of the queues
|
||||
* @param {Object} [notify] show know notifications if set
|
||||
* @param {string} notify.activeQueue current active queue
|
||||
* @param {string} notify.text notification text to show
|
||||
* @param {bool} notify.anyQueue if true show the notification when the comment is shown
|
||||
* in the current active queue besides the 'all' queue.
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleCommentChange(root, comment, sort, notify) {
|
||||
let next = root;
|
||||
const nextQueues = getCommentQueues(comment);
|
||||
|
||||
let notificationShown = false;
|
||||
const showNotificationOnce = () => {
|
||||
if (notificationShown) {
|
||||
return;
|
||||
}
|
||||
notification.info(notify.text);
|
||||
notificationShown = true;
|
||||
};
|
||||
|
||||
queues.forEach((queue) => {
|
||||
if (nextQueues.indexOf(queue) >= 0) {
|
||||
if (!queueHasComment(next, queue, comment.id)) {
|
||||
next = addCommentToQueue(next, queue, comment, sort);
|
||||
if (notify && notify.activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
|
||||
showNotificationOnce(comment);
|
||||
}
|
||||
}
|
||||
} else if(queueHasComment(next, queue, comment.id)){
|
||||
next = removeCommentFromQueue(next, queue, comment.id);
|
||||
if (notify && notify.activeQueue === queue) {
|
||||
showNotificationOnce(comment);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
notify
|
||||
&& (queue === 'all' || notify.anyQueue)
|
||||
&& queueHasComment(next, queue, comment.id)
|
||||
&& notify.activeQueue === queue
|
||||
) {
|
||||
showNotificationOnce(comment);
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from './services/client';
|
||||
import {getClient} from './services/client';
|
||||
import store from './services/store';
|
||||
|
||||
import App from './components/App';
|
||||
@@ -15,7 +15,7 @@ loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
|
||||
render(
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
, document.querySelector('#root')
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
commentId: null,
|
||||
commentStatus: '',
|
||||
};
|
||||
|
||||
export default function banUserDialog(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_BAN_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
};
|
||||
case HIDE_BAN_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_SUSPENDUSER_DIALOG,
|
||||
HIDE_SUSPENDUSER_DIALOG
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = Map({
|
||||
@@ -24,7 +24,7 @@ const initialState = Map({
|
||||
pagePeople: 0,
|
||||
user: Map({}),
|
||||
banDialog: false,
|
||||
suspendDialog: false
|
||||
rejectUsernameDialog: false
|
||||
});
|
||||
|
||||
export default function community (state = initialState, action) {
|
||||
@@ -79,14 +79,14 @@ export default function community (state = initialState, action) {
|
||||
user: Map(action.user),
|
||||
banDialog: true
|
||||
});
|
||||
case HIDE_SUSPENDUSER_DIALOG:
|
||||
case HIDE_REJECT_USERNAME_DIALOG:
|
||||
return state
|
||||
.set('suspendDialog', false);
|
||||
case SHOW_SUSPENDUSER_DIALOG:
|
||||
.set('rejectUsernameDialog', false);
|
||||
case SHOW_REJECT_USERNAME_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
suspendDialog: true
|
||||
rejectUsernameDialog: true
|
||||
});
|
||||
default :
|
||||
return state;
|
||||
|
||||
@@ -5,9 +5,13 @@ import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
import config from './config';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
suspendUserDialog,
|
||||
assets,
|
||||
settings,
|
||||
community,
|
||||
|
||||
@@ -1,57 +1,23 @@
|
||||
import {fromJS, Map, Set} from 'immutable';
|
||||
import {fromJS, Set} from 'immutable';
|
||||
import * as actions from '../constants/moderation';
|
||||
|
||||
const initialState = fromJS({
|
||||
singleView: false,
|
||||
modalOpen: false,
|
||||
user: {},
|
||||
commentId: null,
|
||||
commentStatus: null,
|
||||
userDetailId: null,
|
||||
userDetailActiveTab: 'all',
|
||||
userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
|
||||
userDetailSelectedIds: new Set(),
|
||||
banDialog: false,
|
||||
storySearchVisible: false,
|
||||
storySearchString: '',
|
||||
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
|
||||
sortOrder: 'REVERSE_CHRONOLOGICAL',
|
||||
suspendUserDialog: {
|
||||
show: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
commentId: null,
|
||||
commentStatus: '',
|
||||
},
|
||||
});
|
||||
|
||||
export default function moderation (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.HIDE_BANUSER_DIALOG:
|
||||
return state
|
||||
.set('banDialog', false)
|
||||
.set('commentStatus', null);
|
||||
case actions.SHOW_BANUSER_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
showRejectedNote: action.showRejectedNote,
|
||||
banDialog: true
|
||||
});
|
||||
case actions.SHOW_SUSPEND_USER_DIALOG:
|
||||
return state
|
||||
.mergeDeep({
|
||||
suspendUserDialog: {
|
||||
show: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
}
|
||||
});
|
||||
case actions.HIDE_SUSPEND_USER_DIALOG:
|
||||
return state
|
||||
.setIn(['suspendUserDialog', 'show'], false);
|
||||
case actions.MODERATION_CLEAR_STATE:
|
||||
return initialState;
|
||||
case actions.SET_ACTIVE_TAB:
|
||||
return state
|
||||
.set('activeTab', action.activeTab);
|
||||
@@ -80,6 +46,12 @@ export default function moderation (state = initialState, action) {
|
||||
return state.update('userDetailSelectedIds', (set) => set.add(action.id));
|
||||
case actions.UNSELECT_USER_DETAIL_COMMENT:
|
||||
return state.update('userDetailSelectedIds', (set) => set.delete(action.id));
|
||||
case actions.SHOW_STORY_SEARCH:
|
||||
return state.set('storySearchVisible', true);
|
||||
case actions.HIDE_STORY_SEARCH:
|
||||
return state.set('storySearchVisible', false);
|
||||
case actions.STORY_SEARCH_CHANGE_VALUE:
|
||||
return state.set('storySearchString', action.value);
|
||||
case actions.SET_SORT_ORDER:
|
||||
return state.set('sortOrder', action.order);
|
||||
default :
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog';
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
commentId: null,
|
||||
commentStatus: '',
|
||||
};
|
||||
|
||||
export default function suspendUserDialog(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_SUSPEND_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
};
|
||||
case HIDE_SUSPEND_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './Community.css';
|
||||
import BanUserButton from './BanUserButton';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../Moderation/helpers/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
if (type === 'BAN') {
|
||||
return <BanUserButton icon='not interested' user={user} onClick={() => props.showBanUserDialog(user)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={() => {
|
||||
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showSuspendUserDialog({user: user});
|
||||
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showRejectUsernameDialog({user: user});
|
||||
}}
|
||||
>{t(`modqueue.${menuActionsMap[type].text}`)}</Button>
|
||||
);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.banButton {
|
||||
-webkit-transform: scale(.8);
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import styles from './BanUserButton.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserButton = ({user, ...props}) => (
|
||||
<div className={styles.ban}>
|
||||
<Button cStyle='ban'
|
||||
className={`ban ${styles.banButton}`}
|
||||
disabled={user.status === 'BANNED' ? 'disabled' : ''}
|
||||
onClick={props.onClick}
|
||||
raised>
|
||||
<Icon name='not_interested' />
|
||||
{t('comment.ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
BanUserButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BanUserButton;
|
||||
@@ -1,164 +0,0 @@
|
||||
.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: 500px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 10px;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.ban {
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banuserDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('community.ban_user')}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('community.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('community.are_you_sure', user.username)}</h3>
|
||||
<i>{t('community.note')}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
|
||||
{t('community.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black" className={styles.ban}
|
||||
onClick={() => {
|
||||
handleBanUser({userId: user.id}).then(() => {
|
||||
handleClose();
|
||||
});
|
||||
}}
|
||||
raised>
|
||||
{t('community.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
handleBanUser: PropTypes.func.isRequired,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from './FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
|
||||
export default class Community extends Component {
|
||||
|
||||
@@ -80,19 +79,15 @@ export default class Community extends Component {
|
||||
<FlaggedAccounts
|
||||
commenters={users}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
approveUser={props.approveUser}
|
||||
rejectUsername={props.rejectUsername}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
currentUser={this.props.currentUser}
|
||||
/>
|
||||
<BanUserDialog
|
||||
open={community.banDialog}
|
||||
user={community.user}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
handleBanUser={props.banUser}
|
||||
/>
|
||||
<SuspendUserDialog
|
||||
open={community.suspendDialog}
|
||||
handleClose={props.hideSuspendUserDialog}
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
user={community.user}
|
||||
rejectUsername={props.rejectUsername}
|
||||
/>
|
||||
|
||||
@@ -9,8 +9,6 @@ const FlaggedAccounts = ({...props}) => {
|
||||
const {commenters} = props;
|
||||
const hasResults = commenters && !!commenters.length;
|
||||
|
||||
// if (commenter.status === 'PENDING' && commenter.actions.length > 0) {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
@@ -24,8 +22,9 @@ const FlaggedAccounts = ({...props}) => {
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
approveUser={props.approveUser}
|
||||
suspendUser={props.suspendUser}
|
||||
currentUser={props.currentUser}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
|
||||
|
||||
+17
-17
@@ -1,30 +1,30 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
|
||||
import {Dialog, Button} from 'coral-ui';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const stages = [
|
||||
{
|
||||
title: 'suspenduser.title_reject',
|
||||
description: 'suspenduser.description_reject',
|
||||
title: 'reject_username.title_reject',
|
||||
description: 'reject_username.description_reject',
|
||||
options: {
|
||||
'j': 'suspenduser.no_cancel',
|
||||
'k': 'suspenduser.yes_suspend'
|
||||
'j': 'reject_username.no_cancel',
|
||||
'k': 'reject_username.yes_suspend'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'suspenduser.title_notify',
|
||||
description: 'suspenduser.description_notify',
|
||||
title: 'reject_username.title_notify',
|
||||
description: 'reject_username.description_notify',
|
||||
options: {
|
||||
'j': 'bandialog.cancel',
|
||||
'k': 'suspenduser.send'
|
||||
'k': 'reject_username.send'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class SuspendUserDialog extends Component {
|
||||
class RejectUsernameDialog extends Component {
|
||||
|
||||
state = {email: '', stage: 0}
|
||||
|
||||
@@ -35,7 +35,7 @@ class SuspendUserDialog extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({email: t('suspenduser.email_message_reject'), about: t('suspenduser.username')});
|
||||
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -72,22 +72,22 @@ class SuspendUserDialog extends Component {
|
||||
|
||||
return <Dialog
|
||||
className={styles.suspendDialog}
|
||||
id="suspendUserDialog"
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('suspenduser.suspend_user')}>
|
||||
title={t('reject_username.suspend_user')}>
|
||||
<div className={styles.title}>
|
||||
{t(stages[stage].title, t('suspenduser.username'))}
|
||||
{t(stages[stage].title, t('reject_username.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{t(stages[stage].description, t('suspenduser.username'))}
|
||||
{t(stages[stage].description, t('reject_username.username'))}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{t('suspenduser.write_message')}</div>
|
||||
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
@@ -100,7 +100,7 @@ class SuspendUserDialog extends Component {
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{t(stages[stage].options[key], t('suspenduser.username'))}
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -109,4 +109,4 @@ class SuspendUserDialog extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default SuspendUserDialog;
|
||||
export default RejectUsernameDialog;
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import styles from './Community.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -18,6 +20,16 @@ const User = (props) => {
|
||||
const {user, modActionButtons} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
const showSuspenUserDialog = () => props.showSuspendUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
const showBanUserDialog = () => props.showBanUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
// This means that they have already been reviewed and approved.
|
||||
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
|
||||
@@ -28,12 +40,19 @@ const User = (props) => {
|
||||
<span>
|
||||
{user.username}
|
||||
</span>
|
||||
<ActionButton
|
||||
className={styles.banButton}
|
||||
type='BAN'
|
||||
user={user}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
/>
|
||||
{props.currentUser.id !== user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={showSuspenUserDialog}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,11 +97,10 @@ const User = (props) => {
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={props.approveUser}
|
||||
suspendUser={props.suspendUser}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={props.approveUser}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -91,6 +91,7 @@ span {
|
||||
|
||||
.moderateAsset {
|
||||
a {
|
||||
text-align: center;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
@@ -103,21 +104,9 @@ span {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import {Spinner} from 'coral-ui';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog
|
||||
showRejectUsernameDialog,
|
||||
hideRejectUsernameDialog
|
||||
} from '../../../actions/community';
|
||||
|
||||
import Community from '../components/Community';
|
||||
@@ -85,16 +86,17 @@ export const withCommunityQuery = withQuery(gql`
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community.toJS()
|
||||
community: state.community.toJS(),
|
||||
currentUser: state.auth.toJS().user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
hideRejectUsernameDialog,
|
||||
updateSorting,
|
||||
newPage,
|
||||
}, dispatch);
|
||||
|
||||
@@ -165,7 +165,7 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
checked={settings.autoCloseStream} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{t('configure.close_after')}
|
||||
<div className={styles.settingsHeader}>{t('configure.close_after')}</div>
|
||||
<br />
|
||||
<Textfield
|
||||
type='number'
|
||||
|
||||
@@ -16,6 +16,7 @@ const PermittedDomainsStep = (props) => {
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnBlur={true}
|
||||
addOnPaste={true}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
onChange={(tags) => handleDomainsChange(tags)}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => {
|
||||
e.preventDefault();
|
||||
handleBanUser({userId})
|
||||
.then(handleClose)
|
||||
.then(() => commentStatus === 'REJECTED' ? null : rejectComment({commentId}));
|
||||
};
|
||||
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, commentStatus, showRejectedNote}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banuserDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', user.name)}</h3>
|
||||
<i>{showRejectedNote && t('bandialog.note')}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, commentStatus, handleBanUser, rejectComment, handleClose)} raised>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
handleBanUser: PropTypes.func.isRequired,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
commentId: PropTypes.string,
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {withCopyToClipboard} from 'coral-framework/hocs';
|
||||
|
||||
class ButtonCopyToClipboard extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<Button {...this.props} >
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withCopyToClipboard(ButtonCopyToClipboard);
|
||||
@@ -12,194 +12,239 @@ import {getActionSummary} from 'coral-framework/utils';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import cn from 'classnames';
|
||||
import {murmur3} from 'murmurhash-js';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
const Comment = ({
|
||||
actions = [],
|
||||
comment,
|
||||
viewUserDetail,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
minimal,
|
||||
selected,
|
||||
toggleSelect,
|
||||
...props
|
||||
}) => {
|
||||
const links = linkify.getMatches(comment.body);
|
||||
const linkText = links ? links.map((link) => link.raw) : [];
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions =
|
||||
comment.actions &&
|
||||
comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
let commentType = '';
|
||||
if (comment.status === 'PREMOD') {
|
||||
commentType = 'premod';
|
||||
} else if (flagActions && flagActions.length) {
|
||||
commentType = 'flagged';
|
||||
}
|
||||
class Comment extends React.Component {
|
||||
|
||||
// since words are checked against word boundaries on the backend,
|
||||
// should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support out of the box.
|
||||
const searchWords = [...suspectWords, ...bannedWords]
|
||||
.filter((w) => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
|
||||
})
|
||||
.concat(linkText);
|
||||
render() {
|
||||
const {
|
||||
actions = [],
|
||||
comment,
|
||||
viewUserDetail,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
minimal,
|
||||
selected,
|
||||
toggleSelect,
|
||||
className,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
let selectionStateCSS;
|
||||
if (minimal) {
|
||||
selectionStateCSS = selected ? styles.minimalSelection : '';
|
||||
} else {
|
||||
selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
}
|
||||
const links = linkify.getMatches(comment.body);
|
||||
const linkText = links ? links.map((link) => link.raw) : [];
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions =
|
||||
comment.actions &&
|
||||
comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
let commentType = '';
|
||||
if (comment.status === 'PREMOD') {
|
||||
commentType = 'premod';
|
||||
} else if (flagActions && flagActions.length) {
|
||||
commentType = 'flagged';
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={props.index}
|
||||
className={`mdl-card ${selectionStateCSS} ${styles.Comment} ${styles.listItem} ${minimal ? styles.minimal : ''}`}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
{
|
||||
!minimal && (
|
||||
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
|
||||
{comment.user.name}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
|
||||
<input
|
||||
className={styles.bulkSelectInput}
|
||||
type='checkbox'
|
||||
value={comment.id}
|
||||
checked={selected}
|
||||
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
|
||||
)
|
||||
}
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
|
||||
</span>
|
||||
{props.currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={() => props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
<CommentType type={commentType} />
|
||||
</div>
|
||||
{comment.user.status === 'banned'
|
||||
? <span className={styles.banned}>
|
||||
<Icon name="error_outline" />
|
||||
{t('comment.banned_user')}
|
||||
// since words are checked against word boundaries on the backend,
|
||||
// should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support out of the box.
|
||||
const searchWords = [...suspectWords, ...bannedWords]
|
||||
.filter((w) => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
|
||||
})
|
||||
.concat(linkText);
|
||||
|
||||
let selectionStateCSS;
|
||||
if (minimal) {
|
||||
selectionStateCSS = selected ? styles.minimalSelection : '';
|
||||
} else {
|
||||
selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
}
|
||||
|
||||
const showSuspenUserDialog = () => props.showSuspendUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
|
||||
const showBanUserDialog = () => props.showBanUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={props.index}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.Comment, styles.listItem, {[styles.minimal]: minimal, [styles.selected]: selected})}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
{
|
||||
!minimal && (
|
||||
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
|
||||
{comment.user.username}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
|
||||
<input
|
||||
className={styles.bulkSelectInput}
|
||||
type='checkbox'
|
||||
value={comment.id}
|
||||
checked={selected}
|
||||
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
|
||||
)
|
||||
}
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
|
||||
</span>
|
||||
: null}
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentInfoBar"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset &&
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<Highlighter
|
||||
searchWords={searchWords}
|
||||
textToHighlight={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}#${comment.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentContent"
|
||||
comment={comment}
|
||||
/>
|
||||
<div className={styles.sideActions}>
|
||||
{links
|
||||
? <span className={styles.hasLinks}>
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
{props.currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={showSuspenUserDialog}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
<CommentType type={commentType} />
|
||||
</div>
|
||||
{comment.user.status === 'banned'
|
||||
? <span className={styles.banned}>
|
||||
<Icon name="error_outline" />
|
||||
{t('comment.banned_user')}
|
||||
</span>
|
||||
: null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) => {
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
minimal={minimal}
|
||||
key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: props.acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: props.rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminSideActions"
|
||||
comment={comment}
|
||||
/>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentInfoBar"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset &&
|
||||
<Link to={`/admin/moderate/all/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
|
||||
</div>
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
style={{position: 'relative'}}
|
||||
transitionName={{
|
||||
enter: styles.bodyEnter,
|
||||
enterActive: styles.bodyEnterActive,
|
||||
leave: styles.bodyLeave,
|
||||
leaveActive: styles.bodyLeaveActive,
|
||||
}}
|
||||
transitionEnter={true}
|
||||
transitionLeave={true}
|
||||
transitionEnterTimeout={3600}
|
||||
transitionLeaveTimeout={2800}
|
||||
>
|
||||
<div className={styles.itemBody} key={murmur3(comment.body)}>
|
||||
<p className={styles.body}>
|
||||
<Highlighter
|
||||
searchWords={searchWords}
|
||||
textToHighlight={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}#${comment.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentContent"
|
||||
comment={comment}
|
||||
/>
|
||||
<div className={styles.sideActions}>
|
||||
{links
|
||||
? <span className={styles.hasLinks}>
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
: null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) => {
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
minimal={minimal}
|
||||
key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: props.acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: props.rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminSideActions"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentDetailArea"
|
||||
comment={comment}
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentDetailArea"
|
||||
comment={comment}
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={() => viewUserDetail(comment.user.id)}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Comment.propTypes = {
|
||||
minimal: PropTypes.bool,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
|
||||
@@ -67,3 +67,17 @@
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class FlagBox extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {actionSummaries, actions} = this.props;
|
||||
const {actionSummaries, actions, viewUserDetail} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
|
||||
return (
|
||||
@@ -59,9 +59,14 @@ class FlagBox extends Component {
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{
|
||||
actionList.map((action, j) => <li key={`${i}_${j}`} className={styles.subDetail}><span>{action.user.username}</span> {action.message}</li>)
|
||||
}
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
<a className={styles.username} onClick={viewUserDetail}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -2,18 +2,21 @@ import React, {Component} from 'react';
|
||||
import key from 'keymaster';
|
||||
import styles from './styles.css';
|
||||
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import ModerationQueue from './ModerationQueue';
|
||||
import ModerationMenu from './ModerationMenu';
|
||||
import ModerationHeader from './ModerationHeader';
|
||||
import NotFoundAsset from './NotFoundAsset';
|
||||
import ModerationKeysModal from '../../../components/ModerationKeysModal';
|
||||
import UserDetail from '../containers/UserDetail';
|
||||
import StorySearch from '../containers/StorySearch';
|
||||
|
||||
export default class Moderation extends Component {
|
||||
state = {
|
||||
selectedIndex: 0,
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
selectedIndex: 0
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -24,12 +27,21 @@ export default class Moderation extends Component {
|
||||
key('esc', () => toggleModal(false));
|
||||
key('j', this.select(true));
|
||||
key('k', this.select(false));
|
||||
key('r', this.moderate(false));
|
||||
key('t', this.moderate(true));
|
||||
key('f', this.moderate(false));
|
||||
key('d', this.moderate(true));
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.toggleModal(false);
|
||||
this.props.toggleModal(false);
|
||||
}
|
||||
|
||||
closeSearch = () => {
|
||||
const {toggleStorySearch} = this.props;
|
||||
toggleStorySearch(false);
|
||||
}
|
||||
|
||||
openSearch = () => {
|
||||
this.props.toggleStorySearch(true);
|
||||
}
|
||||
|
||||
moderate = (accept) => () => {
|
||||
@@ -49,24 +61,22 @@ export default class Moderation extends Component {
|
||||
getComments = () => {
|
||||
const {root, route} = this.props;
|
||||
const activeTab = route.path === ':id' ? 'premod' : route.path;
|
||||
return root[activeTab];
|
||||
return root[activeTab].nodes;
|
||||
}
|
||||
|
||||
select = (next) => () => {
|
||||
if (next) {
|
||||
this.setState((prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
selectedIndex: prevState.selectedIndex < this.getComments().length - 1
|
||||
? prevState.selectedIndex + 1 : prevState.selectedIndex
|
||||
}));
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selectedIndex: state.selectedIndex < this.getComments().length - 1
|
||||
? state.selectedIndex + 1 : state.selectedIndex
|
||||
}));
|
||||
} else {
|
||||
this.setState((prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
selectedIndex: prevState.selectedIndex > 0 ?
|
||||
prevState.selectedIndex - 1 : prevState.selectedIndex
|
||||
}));
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selectedIndex: state.selectedIndex > 0
|
||||
? state.selectedIndex - 1 : state.selectedIndex
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +86,8 @@ export default class Moderation extends Component {
|
||||
key.unbind('esc');
|
||||
key.unbind('j');
|
||||
key.unbind('k');
|
||||
key.unbind('r');
|
||||
key.unbind('t');
|
||||
key.unbind('f');
|
||||
key.unbind('d');
|
||||
}
|
||||
|
||||
componentDidUpdate(_, prevState) {
|
||||
@@ -92,19 +102,10 @@ export default class Moderation extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {root, moderation, settings, assets, viewUserDetail, hideUserDetail, ...props} = this.props;
|
||||
const providedAssetId = this.props.params.id;
|
||||
const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path;
|
||||
|
||||
let asset;
|
||||
|
||||
if (providedAssetId) {
|
||||
asset = assets.find((asset) => asset.id === this.props.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return <NotFoundAsset assetId={providedAssetId} />;
|
||||
}
|
||||
}
|
||||
const {root, moderation, settings, viewUserDetail, hideUserDetail, activeTab, ...props} = this.props;
|
||||
const assetId = this.props.params.id;
|
||||
const {asset} = root;
|
||||
|
||||
const comments = root[activeTab];
|
||||
let activeTabCount;
|
||||
@@ -128,7 +129,12 @@ export default class Moderation extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModerationHeader asset={asset} />
|
||||
<ModerationHeader
|
||||
searchVisible={this.props.moderation.storySearchVisible}
|
||||
openSearch={this.openSearch}
|
||||
closeSearch={this.closeSearch}
|
||||
asset={asset}
|
||||
/>
|
||||
<ModerationMenu
|
||||
asset={asset}
|
||||
allCount={root.allCount}
|
||||
@@ -154,36 +160,19 @@ export default class Moderation extends Component {
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
loadMore={props.loadMore}
|
||||
assetId={providedAssetId}
|
||||
assetId={assetId}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
hideUserDetail={hideUserDetail}
|
||||
/>
|
||||
<BanUserDialog
|
||||
open={moderation.banDialog}
|
||||
user={moderation.user}
|
||||
commentId={moderation.commentId}
|
||||
commentStatus={moderation.commentStatus}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
handleBanUser={props.banUser}
|
||||
showRejectedNote={moderation.showRejectedNote}
|
||||
rejectComment={props.rejectComment}
|
||||
/>
|
||||
<SuspendUserDialog
|
||||
open={moderation.suspendUserDialog.show}
|
||||
username={moderation.suspendUserDialog.username}
|
||||
userId={moderation.suspendUserDialog.userId}
|
||||
organizationName={root.settings.organizationName}
|
||||
onCancel={props.hideSuspendUserDialog}
|
||||
onPerform={this.props.suspendUser}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
hideShortcutsNote={props.hideShortcutsNote}
|
||||
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
|
||||
open={moderation.modalOpen}
|
||||
onClose={this.onClose}/>
|
||||
|
||||
{moderation.userDetailId && (
|
||||
<UserDetail
|
||||
id={moderation.userDetailId}
|
||||
@@ -195,6 +184,13 @@ export default class Moderation extends Component {
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment} />
|
||||
)}
|
||||
|
||||
<StorySearch
|
||||
assetId={assetId}
|
||||
moderation={this.props.moderation}
|
||||
closeSearch={this.closeSearch}
|
||||
storySearchChange={this.props.storySearchChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ModerationHeader = (props) => (
|
||||
const ModerationHeader = ({asset, searchVisible, openSearch, closeSearch}) => {
|
||||
|
||||
const trigger = searchVisible ? closeSearch : openSearch;
|
||||
const searchTriggerIcon = <Icon className={styles.searchTrigger} name={searchVisible ? 'arrow_drop_up' : 'arrow_drop_down'} />;
|
||||
|
||||
const title = asset
|
||||
? <a onClick={trigger} className="mdl-tabs__tab">{asset.title} {searchTriggerIcon}</a>
|
||||
: <a onClick={trigger} className="mdl-tabs__tab">{t('modqueue.all_streams')} {searchTriggerIcon}</a>;
|
||||
|
||||
return (
|
||||
<div className=''>
|
||||
<div className={`mdl-tabs ${styles.header}`}>
|
||||
{
|
||||
props.asset ?
|
||||
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
|
||||
<Link className="mdl-tabs__tab" to="/admin/moderate">{t('modqueue.all_streams')}</Link>
|
||||
<a className="mdl-tabs__tab" href={props.asset.url} target="_blank">
|
||||
<span>{props.asset.title}</span>
|
||||
<Icon className={styles.settingsButton} name="open_in_new"/>
|
||||
</a>
|
||||
<Link className="mdl-tabs__tab" to="/admin/stories">Select Stream</Link>
|
||||
</div>
|
||||
:
|
||||
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
|
||||
<a className="mdl-tabs__tab" />
|
||||
<a className="mdl-tabs__tab">{t('modqueue.all_streams')}</a>
|
||||
<Link className="mdl-tabs__tab" to="/admin/stories">{t('modqueue.select_stream')}</Link>
|
||||
</div>
|
||||
}
|
||||
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
ModerationHeader.propTypes = {
|
||||
asset: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
}),
|
||||
openSearch: PropTypes.func.isRequired,
|
||||
closeSearch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ModerationHeader;
|
||||
|
||||
@@ -6,8 +6,10 @@ import EmptyCard from '../../../components/EmptyCard';
|
||||
import {actionsMap} from '../helpers/moderationQueueActionsMap';
|
||||
import LoadMore from './LoadMore';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
|
||||
class ModerationQueue extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
static propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
@@ -22,7 +24,19 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
this.props.loadMore(this.props.activeTab);
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
this.props.loadMore(this.props.activeTab)
|
||||
.then(() => this.isLoadingMore = false)
|
||||
.catch((e) => {
|
||||
this.isLoadingMore = false;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
@@ -43,20 +57,34 @@ class ModerationQueue extends React.Component {
|
||||
commentCount,
|
||||
singleView,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul style={{paddingLeft: 0}}>
|
||||
<CSSTransitionGroup
|
||||
key={activeTab}
|
||||
component={'ul'}
|
||||
style={{paddingLeft: 0}}
|
||||
transitionName={{
|
||||
enter: styles.commentEnter,
|
||||
enterActive: styles.commentEnterActive,
|
||||
leave: styles.commentLeave,
|
||||
leaveActive: styles.commentLeaveActive,
|
||||
}}
|
||||
transitionEnter={true}
|
||||
transitionLeave={true}
|
||||
transitionEnterTimeout={1000}
|
||||
transitionLeaveTimeout={1000}
|
||||
>
|
||||
{
|
||||
comments.length
|
||||
? comments.map((comment, i) => {
|
||||
comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={i}
|
||||
key={comment.id}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={i === selectedIndex}
|
||||
@@ -72,9 +100,14 @@ class ModerationQueue extends React.Component {
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
|
||||
}
|
||||
</ul>
|
||||
</CSSTransitionGroup>
|
||||
{comments.length === 0 &&
|
||||
<div className={styles.emptyCardContainer}>
|
||||
<EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
|
||||
</div>
|
||||
}
|
||||
|
||||
<LoadMore
|
||||
loadMore={this.loadMore}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './StorySearch.css';
|
||||
|
||||
const formatDate = (date) => {
|
||||
const d = new Date(date);
|
||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
||||
};
|
||||
|
||||
const Story = ({author, title, createdAt, open, id, goToStory}) => {
|
||||
return (
|
||||
<li className={styles.story} onClick={() => goToStory(id)}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.author}>By {author}</span>
|
||||
<span className={styles.createdAt}>{formatDate(createdAt)}</span>
|
||||
<span className={styles.status}>{open ? 'Open' : 'Closed'}</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
Story.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
author: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
open: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Story;
|
||||
@@ -0,0 +1,147 @@
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 58px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
top: 106px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 50%;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
z-index: 100;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.positionShim {
|
||||
left: -50%;
|
||||
}
|
||||
|
||||
.headInput {
|
||||
background-color: #efefef;
|
||||
padding: 10px 27px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: calc(100% - 100px);
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #dfdfdf;
|
||||
max-height: 45px;
|
||||
max-width: 600px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cta > a {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 12px 30px;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
|
||||
/*.storyList {
|
||||
border-top: 1px solid #ddd;
|
||||
}*/
|
||||
|
||||
.story {
|
||||
padding: 7px 50px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 400ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title, .meta {
|
||||
margin: 0;
|
||||
color: black;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.author, .createdAt, .status {
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
width: 90px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
padding: 7px 27px;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
.searchResults i {
|
||||
font-size: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.accessStories {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.headlineRecent {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.25px;
|
||||
vertical-align: middle;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
padding: 10px 24px 15px 49px;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './StorySearch.css';
|
||||
import {Button, Spinner, Icon} from 'coral-ui';
|
||||
import Story from './Story';
|
||||
|
||||
const StorySearch = (props) => {
|
||||
|
||||
const {
|
||||
root: {
|
||||
assets = []
|
||||
},
|
||||
data: {loading}
|
||||
} = props;
|
||||
|
||||
if (!props.moderation.storySearchVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.container} role='alertdialog' onKeyDown={props.handleEsc}>
|
||||
<div className={styles.positionShim}>
|
||||
<div className={styles.headInput}>
|
||||
<input
|
||||
className={styles.searchInput}
|
||||
onChange={props.handleSearchChange}
|
||||
onKeyDown={props.handleEnter}
|
||||
value={props.searchValue}
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
cStyle='blue'
|
||||
className={styles.searchButton}
|
||||
onClick={props.search}
|
||||
raised >
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
{props.assetId &&
|
||||
<div className={styles.cta}>
|
||||
<a onClick={props.goToModerateAll}>Moderate comments on All Stories</a>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.storyList}>
|
||||
|
||||
{
|
||||
props.moderation.storySearchString ? (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="search" />
|
||||
<span className={styles.headlineRecent}>Search Results</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="access_time" />
|
||||
<span className={styles.headlineRecent}>Most Recent Stories</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
loading
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
return <Story
|
||||
key={i}
|
||||
id={story.id}
|
||||
title={story.title}
|
||||
createdAt={new Date(story.created_at).toISOString()}
|
||||
open={storyOpen}
|
||||
author={story.author}
|
||||
goToStory={props.goToStory}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
|
||||
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.overlay} onClick={props.clearAndCloseSearch} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StorySearch.propTypes = {
|
||||
search: PropTypes.func.isRequired,
|
||||
goToStory: PropTypes.func.isRequired,
|
||||
clearAndCloseSearch: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
handleSearchChange: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string
|
||||
};
|
||||
|
||||
export default StorySearch;
|
||||
@@ -35,9 +35,8 @@
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
width: 90%;
|
||||
outline: none;
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.commentStatuses {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Drawer} from 'coral-ui';
|
||||
import styles from './UserDetail.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import Comment from './Comment';
|
||||
import styles from './UserDetail.css';
|
||||
import {Button, Drawer} from 'coral-ui';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import {actionsMap} from '../helpers/moderationQueueActionsMap';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
|
||||
export default class UserDetail extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
@@ -22,16 +25,6 @@ export default class UserDetail extends React.Component {
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
copyPermalink = () => {
|
||||
this.profile.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (e) {
|
||||
|
||||
/* nothing */
|
||||
}
|
||||
}
|
||||
|
||||
rejectThenReload = (info) => {
|
||||
this.props.rejectComment(info).then(() => {
|
||||
this.props.data.refetch();
|
||||
@@ -88,86 +81,92 @@ export default class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer handleClickOutside={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
<Button className={styles.copyButton} onClick={this.copyPermalink}>Copy</Button>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
user={user}
|
||||
/>
|
||||
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<strong>Account summary</strong>
|
||||
<br/><small className={styles.small}>Data represents the last six months of activity</small>
|
||||
</p>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.stat}>
|
||||
<p>Total Comments</p>
|
||||
<p>{totalComments}</p>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<p>Reject Rate</p>
|
||||
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
selectedIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ClickOutside onClickOutside={hideUserDetail}>
|
||||
<Drawer onClose={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<ButtonCopyToClipboard className={styles.copyButton} copyText={profile} />
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
user={user}
|
||||
/>
|
||||
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<strong>Account summary</strong>
|
||||
<br/><small className={styles.small}>Data represents the last six months of activity</small>
|
||||
</p>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.stat}>
|
||||
<p>Total Comments</p>
|
||||
<p>{totalComments}</p>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<p>Reject Rate</p>
|
||||
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
nodes.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
const selected = selectedIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={false}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
viewUserDetail={() => {}}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
currentAsset={null}
|
||||
currentUserId={this.props.id}
|
||||
minimal={true} />;
|
||||
})
|
||||
selectedIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<div>
|
||||
{
|
||||
nodes.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
const selected = selectedIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={false}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
viewUserDetail={() => {}}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
currentAsset={null}
|
||||
currentUserId={this.props.id}
|
||||
minimal={true} />;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,29 +116,20 @@ span {
|
||||
transition: background-color 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #212121;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 344px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #212121;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 344px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,8 +190,6 @@ span {
|
||||
}
|
||||
|
||||
&.selected {
|
||||
max-width: 720px;
|
||||
max-height: 410px;
|
||||
}
|
||||
|
||||
.context {
|
||||
@@ -479,3 +468,63 @@ span {
|
||||
.bulkSelectInput {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emptyCardContainer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.commentLeave {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentLeaveActive {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.commentEnter {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnterActive {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.bodyLeave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
opacity: 1.0;
|
||||
transition: background 400ms, opacity 800ms 1600ms;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bodyLeaveActive {
|
||||
opacity: 0;
|
||||
background-color: rgba(255,255,0, 0.2);
|
||||
}
|
||||
|
||||
.bodyEnter {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bodyEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms 2400ms;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.searchTrigger {
|
||||
position: relative;
|
||||
top: .3em;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default withFragments({
|
||||
status
|
||||
user {
|
||||
id
|
||||
name: username
|
||||
username
|
||||
status
|
||||
}
|
||||
asset {
|
||||
@@ -49,6 +49,9 @@ export default withFragments({
|
||||
}
|
||||
}
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
${pluginFragments.spreads('comment')}
|
||||
}
|
||||
${pluginFragments.definitions('comment')}
|
||||
|
||||
@@ -2,75 +2,137 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import update from 'immutability-helper';
|
||||
import truncate from 'lodash/truncate';
|
||||
import NotFoundAsset from '../components/NotFoundAsset';
|
||||
|
||||
import {withSetUserStatus, withSuspendUser, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {handleCommentChange} from '../../../graphql/utils';
|
||||
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
import {updateAssets} from 'actions/assets';
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
import {
|
||||
toggleModal,
|
||||
singleView,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
hideShortcutsNote,
|
||||
toggleStorySearch,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState
|
||||
} from 'actions/moderation';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
import Moderation from '../components/Moderation';
|
||||
import Comment from './Comment';
|
||||
|
||||
function prepareNotificationText(text) {
|
||||
return truncate(text, {length: 50}).replace('\n', ' ');
|
||||
}
|
||||
|
||||
class ModerationContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
get activeTab() { return this.props.route.path; }
|
||||
|
||||
subscribeToUpdates(variables = this.props.data.variables) {
|
||||
const sub1 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const sort = this.props.moderation.sortOrder;
|
||||
const notify = this.props.auth.user.id === user.id
|
||||
? {}
|
||||
: {
|
||||
activeQueue: this.activeTab,
|
||||
text: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)),
|
||||
anyQueue: false,
|
||||
};
|
||||
return handleCommentChange(prev, comment, sort, notify);
|
||||
},
|
||||
});
|
||||
|
||||
const sub2 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const sort = this.props.moderation.sortOrder;
|
||||
const notify = this.props.auth.user.id === user.id
|
||||
? {}
|
||||
: {
|
||||
activeQueue: this.activeTab,
|
||||
text: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)),
|
||||
anyQueue: false,
|
||||
};
|
||||
return handleCommentChange(prev, comment, sort, notify);
|
||||
},
|
||||
});
|
||||
|
||||
const sub3 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
const sort = this.props.moderation.sortOrder;
|
||||
const notify = {
|
||||
activeQueue: this.activeTab,
|
||||
text: t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)),
|
||||
anyQueue: false,
|
||||
};
|
||||
return handleCommentChange(prev, comment, sort, notify);
|
||||
},
|
||||
});
|
||||
|
||||
const sub4 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
const user = comment.actions[comment.actions.length - 1].user;
|
||||
const sort = this.props.moderation.sortOrder;
|
||||
const notify = {
|
||||
activeQueue: this.activeTab,
|
||||
text: t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)),
|
||||
anyQueue: true,
|
||||
};
|
||||
return handleCommentChange(prev, comment, sort, notify);
|
||||
},
|
||||
});
|
||||
|
||||
this.subscriptions.push(sub1, sub2, sub3, sub4);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
resubscribe(variables) {
|
||||
this.unsubscribe();
|
||||
this.subscribeToUpdates(variables);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.clearState();
|
||||
this.props.fetchSettings();
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {updateAssets} = this.props;
|
||||
if(!isEqual(nextProps.root.assets, this.props.root.assets)) {
|
||||
updateAssets(nextProps.root.assets);
|
||||
}
|
||||
}
|
||||
|
||||
suspendUser = async (args) => {
|
||||
this.props.hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await this.props.suspendUser(args);
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
notification.success(
|
||||
t('suspenduser.notify_suspend_until',
|
||||
this.props.moderation.suspendUserDialog.username,
|
||||
timeago(args.until)),
|
||||
);
|
||||
const {commentStatus, commentId} = this.props.moderation.suspendUserDialog;
|
||||
if (commentStatus !== 'REJECTED') {
|
||||
return this.props.rejectComment({commentId})
|
||||
.then((result) => {
|
||||
if (result.data.setCommentStatus.errors) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Resubscribe when we change between assets.
|
||||
if(this.props.data.variables.asset_id !== nextProps.data.variables.asset_id) {
|
||||
this.resubscribe(nextProps.data.variables);
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
}
|
||||
};
|
||||
|
||||
banUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'BANNED'});
|
||||
}
|
||||
|
||||
acceptComment = ({commentId}) => {
|
||||
@@ -113,6 +175,9 @@ class ModerationContainer extends Component {
|
||||
return update(prev, {
|
||||
[tab]: {
|
||||
nodes: {$push: comments.nodes},
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
startCursor: {$set: comments.startCursor},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -120,39 +185,100 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
render () {
|
||||
const {root, data} = this.props;
|
||||
const {root, root: {asset}, data, params: {id: assetId}} = this.props;
|
||||
|
||||
if (data.error) {
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
if (!('premodCount' in root)) {
|
||||
return <div><Spinner/></div>;
|
||||
if (assetId) {
|
||||
if (asset === null) {
|
||||
|
||||
// Not found.
|
||||
return <NotFoundAsset assetId={assetId} />;
|
||||
}
|
||||
if (asset === undefined || asset.id !== assetId) {
|
||||
|
||||
// Still loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
} else if(asset !== undefined || !('premodCount' in root)) {
|
||||
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return <Moderation
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
banUser={this.banUser}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
suspendUser={this.suspendUser}
|
||||
activeTab={this.activeTab}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT_EDITED_SUBSCRIPTION = gql`
|
||||
subscription CommentEdited($asset_id: ID){
|
||||
commentEdited(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const COMMENT_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription CommentFlagged($asset_id: ID){
|
||||
commentFlagged(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
|
||||
subscription CommentAccepted($asset_id: ID){
|
||||
commentAccepted(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription CommentRejected($asset_id: ID){
|
||||
commentRejected(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -171,7 +297,7 @@ const commentConnectionFragment = gql`
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER) {
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
|
||||
all: comments(query: {
|
||||
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
|
||||
asset_id: $asset_id,
|
||||
@@ -208,7 +334,7 @@ const withModQueueQuery = withQuery(gql`
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
assets: assets {
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
url
|
||||
@@ -244,6 +370,7 @@ const withModQueueQuery = withQuery(gql`
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort: sortOrder,
|
||||
allAssets: id === null
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -287,31 +414,28 @@ const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation.toJS(),
|
||||
settings: state.settings.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
assets: state.assets.get('assets')
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
toggleModal,
|
||||
singleView,
|
||||
updateAssets,
|
||||
fetchSettings,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
hideShortcutsNote,
|
||||
toggleStorySearch,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetCommentStatus,
|
||||
withSetUserStatus,
|
||||
withSuspendUser,
|
||||
withQueueCountPolling,
|
||||
withModQueueQuery,
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import StorySearch from '../components/StorySearch';
|
||||
import {withRouter} from 'react-router';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {isEmpty} from 'lodash';
|
||||
|
||||
class StorySearchContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
searchValue: props.moderation.storySearchString
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.storySearchChange('');
|
||||
}
|
||||
|
||||
clearSearch = () => {
|
||||
this.setState({searchValue: ''}, () => {
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
|
||||
clearAndCloseSearch = () => {
|
||||
if (!isEmpty(this.state.searchValue)) {
|
||||
this.clearSearch();
|
||||
}
|
||||
this.props.closeSearch();
|
||||
}
|
||||
|
||||
handleSearchChange = (e) => {
|
||||
const {value} = e.target;
|
||||
this.setState({
|
||||
searchValue: value
|
||||
});
|
||||
}
|
||||
|
||||
handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.clearAndCloseSearch();
|
||||
}
|
||||
}
|
||||
|
||||
handleEnter = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
search = () => {
|
||||
const {searchValue} = this.state;
|
||||
this.props.storySearchChange(searchValue);
|
||||
}
|
||||
|
||||
goToStory = (id) => {
|
||||
const {router} = this.props;
|
||||
router.push(`/admin/moderate/all/${id}`);
|
||||
this.clearAndCloseSearch();
|
||||
}
|
||||
|
||||
goToModerateAll = () => {
|
||||
const {router} = this.props;
|
||||
router.push('/admin/moderate/all');
|
||||
this.clearAndCloseSearch();
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<StorySearch
|
||||
search={this.search}
|
||||
goToStory={this.goToStory}
|
||||
goToModerateAll={this.goToModerateAll}
|
||||
handleEsc={this.handleEsc}
|
||||
handleEnter={this.handleEnter}
|
||||
searchValue={this.state.searchValue}
|
||||
handleSearchChange={this.handleSearchChange}
|
||||
clearAndCloseSearch={this.clearAndCloseSearch}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const withAssetSearchQuery = withQuery(gql`
|
||||
query SearchStories($value: String = "") {
|
||||
assets(query: {value: $value, limit: 10}) {
|
||||
id
|
||||
title
|
||||
url
|
||||
created_at
|
||||
closedAt
|
||||
author
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({moderation: {storySearchString = ''}}) => {
|
||||
return {
|
||||
variables: {
|
||||
value: storySearchString
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
withAssetSearchQuery
|
||||
)(StorySearchContainer);
|
||||
@@ -5,9 +5,9 @@ export const actionsMap = {
|
||||
};
|
||||
|
||||
export const menuActionsMap = {
|
||||
'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'r'},
|
||||
'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'f'},
|
||||
'REJECTED': {status: 'REJECTED', text: 'rejected', icon: 'close'},
|
||||
'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 't'},
|
||||
'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 'd'},
|
||||
'FLAGGED': {status: 'FLAGGED', text: 'flag', icon: 'flag', filter: 'Untouched'},
|
||||
'BAN': {status: 'BANNED', text: 'ban_user', icon: 'not interested'},
|
||||
'': {icon: 'done'}
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import {networkInterface} from 'coral-framework/services/transport';
|
||||
import {getClient as getFrameworkClient} from 'coral-framework/services/client';
|
||||
import fragmentMatcher from './fragmentMatcher';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
fragmentMatcher,
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface
|
||||
});
|
||||
|
||||
export default client;
|
||||
|
||||
export function getClient() {
|
||||
return getFrameworkClient({fragmentMatcher});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {client} from './client';
|
||||
import {getClient} from './client';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(client.middleware()),
|
||||
applyMiddleware(getClient().middleware()),
|
||||
applyMiddleware(thunk)
|
||||
];
|
||||
|
||||
@@ -16,7 +16,7 @@ if (window.devToolsExtension) {
|
||||
|
||||
const coralReducers = {
|
||||
...mainReducer,
|
||||
apollo: client.reducer()
|
||||
apollo: getClient().reducer()
|
||||
};
|
||||
|
||||
const store = createStore(
|
||||
|
||||
@@ -26,6 +26,7 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import {EditableCommentContent} from './EditableCommentContent';
|
||||
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF');
|
||||
const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length;
|
||||
@@ -75,6 +76,8 @@ const ActionButton = ({children}) => {
|
||||
};
|
||||
|
||||
export default class Comment extends React.Component {
|
||||
isLoadingReplies = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -210,14 +213,24 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
loadNewReplies = () => {
|
||||
const {replies, replyCount, id} = this.props.comment;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.props.loadMore(id).then(() => {
|
||||
this.setState(resetCursors(this.state, this.props));
|
||||
});
|
||||
return;
|
||||
if (!this.isLoadingReplies) {
|
||||
this.isLoadingReplies = true;
|
||||
const {replies, replyCount, id} = this.props.comment;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.props.loadMore(id)
|
||||
.then(() => {
|
||||
this.setState(resetCursors(this.state, this.props));
|
||||
this.isLoadingReplies = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.isLoadingReplies = false;
|
||||
throw e;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState(resetCursors);
|
||||
this.isLoadingReplies = false;
|
||||
}
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
showReplyBox = () => {
|
||||
@@ -415,7 +428,7 @@ export default class Comment extends React.Component {
|
||||
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>(Edited)</span></span>
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
@@ -431,28 +444,28 @@ export default class Comment extends React.Component {
|
||||
inline
|
||||
/>
|
||||
|
||||
{ (currentUser &&
|
||||
(comment.user.id === currentUser.id))
|
||||
{ (currentUser && (comment.user.id === currentUser.id)) &&
|
||||
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
? <span className={cn(styles.topRight)}>
|
||||
{
|
||||
commentIsStillEditable(comment) &&
|
||||
<a
|
||||
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
|
||||
onClick={this.onClickEdit}>Edit</a>
|
||||
}
|
||||
</span>
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
<span className={cn(styles.topRight)}>
|
||||
{
|
||||
commentIsStillEditable(comment) &&
|
||||
<a
|
||||
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
|
||||
onClick={this.onClickEdit}>Edit</a>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
{ (currentUser && (comment.user.id !== currentUser.id)) &&
|
||||
|
||||
/* TopRightMenu allows currentUser to ignore other users' comments */
|
||||
: <span className={cn(styles.topRight, styles.topRightMenu)}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
addNotification={addNotification} />
|
||||
</span>
|
||||
<span className={cn(styles.topRight, styles.topRightMenu)}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
addNotification={addNotification} />
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
this.state.isEditing
|
||||
? <EditableCommentContent
|
||||
|
||||
@@ -80,14 +80,17 @@ export class EditableCommentContent extends React.Component {
|
||||
}
|
||||
successfullyEdited = true;
|
||||
} catch (error) {
|
||||
if (error.translation_key) {
|
||||
addNotification('error', t(`error.${error.translation_key}`));
|
||||
} else if (error.networkError) {
|
||||
addNotification('error', t('error.network_error'));
|
||||
} else {
|
||||
addNotification('error', t('edit_comment.unexpected_error'));
|
||||
throw error;
|
||||
}
|
||||
const errors = error.errors || [error];
|
||||
errors.forEach((e) => {
|
||||
if (e.translation_key) {
|
||||
addNotification('error', t(`error.${e.translation_key}`));
|
||||
} else if (error.networkError) {
|
||||
addNotification('error', t('error.network_error'));
|
||||
} else {
|
||||
addNotification('error', t('edit_comment.unexpected_error'));
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (successfullyEdited) {
|
||||
const status = response.data.editComment.comment.status;
|
||||
|
||||
@@ -23,15 +23,17 @@ class LoadMore extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
this.initialState = false;
|
||||
this.props.loadMore();
|
||||
}
|
||||
|
||||
render () {
|
||||
const {topLevel, moreComments, loadMore, replyCount} = this.props;
|
||||
const {topLevel, moreComments, replyCount} = this.props;
|
||||
return moreComments
|
||||
? <div className='coral-load-more'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.initialState = false;
|
||||
loadMore();
|
||||
}}>
|
||||
onClick={this.loadMore}>
|
||||
{topLevel ? t('framework.view_more_comments') : this.replyCountFormat(replyCount)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,8 @@ function invalidateCursor(invalidated, state, props) {
|
||||
|
||||
class Stream extends React.Component {
|
||||
|
||||
isLoadingMore = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = resetCursors(this.state, props);
|
||||
@@ -95,6 +97,18 @@ class Stream extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
this.props.loadMoreComments()
|
||||
.then(() => this.isLoadingMore = false)
|
||||
.catch((e) => {
|
||||
this.isLoadingMore = false;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which were authored by current user or comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
@@ -159,7 +173,7 @@ class Stream extends React.Component {
|
||||
};
|
||||
return (
|
||||
<div id="stream">
|
||||
<Slot fill="stream" />
|
||||
|
||||
{open
|
||||
? <div id="commentBox">
|
||||
<InfoBox
|
||||
@@ -183,6 +197,7 @@ class Stream extends React.Component {
|
||||
<SuspendedAccount
|
||||
canEditName={user && user.canEditName}
|
||||
editName={editName}
|
||||
currentUsername={user.username}
|
||||
/>}
|
||||
{loggedIn &&
|
||||
!banned &&
|
||||
@@ -203,6 +218,8 @@ class Stream extends React.Component {
|
||||
</div>
|
||||
: <p>{asset.settings.closedMessage}</p>}
|
||||
|
||||
<Slot fill="stream" />
|
||||
|
||||
{loggedIn && (
|
||||
<ModerationLink
|
||||
assetId={asset.id}
|
||||
@@ -286,7 +303,7 @@ class Stream extends React.Component {
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
moreComments={asset.comments.hasNextPage}
|
||||
loadMore={this.props.loadMoreComments}
|
||||
loadMore={this.loadMoreComments}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ class SuspendedAccount extends Component {
|
||||
|
||||
static propTypes = {
|
||||
canEditName: PropTypes.bool,
|
||||
editName: PropTypes.func.isRequired
|
||||
editName: PropTypes.func.isRequired,
|
||||
currentUsername: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
@@ -21,7 +22,11 @@ class SuspendedAccount extends Component {
|
||||
const {editName} = this.props;
|
||||
const {username} = this.state;
|
||||
e.preventDefault();
|
||||
if (validate.username(username)) {
|
||||
|
||||
if (username === this.props.currentUsername) {
|
||||
this.setState({alert: t('error.SAME_USERNAME_PROVIDED')});
|
||||
}
|
||||
else if (validate.username(username)) {
|
||||
editName(username)
|
||||
.then(() => location.reload())
|
||||
.catch((error) => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import styles from './Toggleable.css';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
|
||||
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
|
||||
|
||||
export default class Toggleable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.setState({isOpen: !this.state.isOpen});
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.setState({isOpen: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {isOpen} = this.state;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.close}>
|
||||
<span className={styles.Toggleable} tabIndex="0" >
|
||||
<span className={styles.toggler}
|
||||
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
|
||||
{isOpen ? children : null}
|
||||
</span>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import {IgnoreUserWizard} from './IgnoreUserWizard';
|
||||
import styles from './TopRightMenu.css';
|
||||
import Toggleable from './Toggleable';
|
||||
|
||||
// TopRightMenu appears as a dropdown in the top right of the comment.
|
||||
// when you click the down cehvron, it expands and shows IgnoreUserWizard
|
||||
@@ -56,38 +54,6 @@ export class TopRightMenu extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</Toggleable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
|
||||
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
|
||||
class Toggleable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
this.state = {
|
||||
isOpen: false
|
||||
};
|
||||
}
|
||||
toggle() {
|
||||
this.setState({isOpen: !this.state.isOpen});
|
||||
}
|
||||
close() {
|
||||
this.setState({isOpen: false});
|
||||
}
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {isOpen} = this.state;
|
||||
return (
|
||||
|
||||
// /*onBlur={ this.close } */
|
||||
<span className={styles.Toggleable} tabIndex="0" >
|
||||
<span className={styles.toggler}
|
||||
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
|
||||
{isOpen ? children : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ body {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#coralStream {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.expandForSignin {
|
||||
min-height: 600px;
|
||||
}
|
||||
@@ -183,7 +187,6 @@ hr {
|
||||
|
||||
.talk-stream-comments-container {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.commentStream {
|
||||
@@ -479,11 +482,6 @@ button.comment__action-button[disabled],
|
||||
}
|
||||
|
||||
@media (min-device-width : 300px) and (max-device-width : 420px) {
|
||||
.commentActionsLeft.comment__action-container .coral-plugin-likes-button-text,
|
||||
.commentActionsLeft.comment__action-container > div span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button {
|
||||
visibility: collapse;
|
||||
margin-left: -30px;
|
||||
|
||||
@@ -54,6 +54,12 @@ function configurePymParent(pymParent, opts) {
|
||||
pymParent.sendMessage('config', JSON.stringify(config));
|
||||
}
|
||||
|
||||
pymParent.onMessage('coral-auth-changed', function(message) {
|
||||
if (opts.onAuthChanged) {
|
||||
opts.onAuthChanged(message ? JSON.parse(message) : null);
|
||||
}
|
||||
});
|
||||
|
||||
// Sends config to the child
|
||||
pymParent.onMessage('getConfig', function() {
|
||||
sendConfig(opts || {});
|
||||
@@ -67,6 +73,11 @@ function configurePymParent(pymParent, opts) {
|
||||
|
||||
window.document.body.appendChild(snackbar);
|
||||
|
||||
// Notify embed that there was a click outside.
|
||||
document.addEventListener('click', () => {
|
||||
pymParent.sendMessage('click');
|
||||
}, true);
|
||||
|
||||
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
|
||||
pymParent.el.firstChild.style.width = '1px';
|
||||
pymParent.el.firstChild.style.minWidth = '100%';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const fetchAssetFailure = (error) => ({type: actions.FETCH_ASSET_FAILURE,
|
||||
|
||||
const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_REQUEST});
|
||||
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
|
||||
const updateAssetSettingsFailure = () => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE});
|
||||
const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error});
|
||||
|
||||
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
const assetId = getState().asset.toJS().id;
|
||||
@@ -20,7 +20,10 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(updateAssetSettingsSuccess(newConfig));
|
||||
})
|
||||
.catch((error) => dispatch(updateAssetSettingsFailure(error)));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(updateAssetSettingsFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
|
||||
@@ -31,7 +34,10 @@ export const updateOpenStream = (closedBody) => (dispatch, getState) => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(fetchAssetSuccess(closedBody));
|
||||
})
|
||||
.catch((error) => dispatch(fetchAssetFailure(error)));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(fetchAssetFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
const openStream = () => ({type: actions.OPEN_COMMENTS});
|
||||
|
||||
@@ -3,6 +3,7 @@ import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import * as Storage from '../helpers/storage';
|
||||
import coralApi, {base} from '../helpers/request';
|
||||
import pym from '../services/PymConnection';
|
||||
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -74,7 +75,9 @@ export const createUsername = (userId, formData) => (dispatch) => {
|
||||
dispatch(updateUsername(formData));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(createUsernameFailure(t(`error.${error.translation_key}`)));
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(createUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -138,16 +141,20 @@ export const fetchSignIn = (formData) => {
|
||||
dispatch(hideSignInDialog());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
if (error.metadata) {
|
||||
|
||||
// the user might not have a valid email. prompt the user user re-request the confirmation email
|
||||
dispatch(
|
||||
signInFailure(t('error.email_not_verified', error.metadata))
|
||||
);
|
||||
} else {
|
||||
} else if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
|
||||
// invalid credentials
|
||||
dispatch(signInFailure(t('error.email_password')));
|
||||
dispatch(signInFailure(t('error.email_password'), error.metadata));
|
||||
} else {
|
||||
const str = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(signInFailure(str));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -233,12 +240,8 @@ export const fetchSignUp = (formData) => (dispatch, getState) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
let errorMessage = t(`error.${error.message}`);
|
||||
|
||||
// if there is no translation defined, just show the error string
|
||||
if (errorMessage === `error.${error.message}`) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(signUpFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -255,8 +258,9 @@ const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
@@ -267,7 +271,11 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
body: {email, loc: redirectUri}
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => dispatch(forgotPasswordFailure(error)));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -284,6 +292,7 @@ export const logout = () => (dispatch) => {
|
||||
resetWebsocket();
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -315,6 +324,7 @@ export const checkLogin = () => (dispatch) => {
|
||||
resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
|
||||
// Display create username dialog if necessary.
|
||||
if (result.user.canEditName && result.user.status !== 'BANNED') {
|
||||
@@ -323,7 +333,8 @@ export const checkLogin = () => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -357,11 +368,10 @@ export const requestConfirmEmail = (email) => (dispatch, getState) => {
|
||||
.then(() => {
|
||||
dispatch(verifyEmailSuccess());
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
// email might have already been verifyed
|
||||
dispatch(verifyEmailFailure(err));
|
||||
throw err;
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(verifyEmailFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export const editName = (username) => (dispatch) => {
|
||||
dispatch(addNotification('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(editUsernameFailure(t(`error.${error.translation_key}`)));
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(editUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {Component, cloneElement, Children} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {pym} from 'coral-framework';
|
||||
|
||||
export default class ClickOutside extends Component {
|
||||
static propTypes = {
|
||||
onClickOutside: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
domNode = null;
|
||||
|
||||
handleClick = (e) => {
|
||||
const {onClickOutside} = this.props;
|
||||
if (!e || !this.domNode.contains(e.target)) {
|
||||
onClickOutside(e);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.domNode = findDOMNode(this);
|
||||
document.addEventListener('click', this.handleClick, true);
|
||||
pym.onMessage('click', this.handleClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleClick, true);
|
||||
pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, onClickOutside: _, ...rest} = this.props;
|
||||
return cloneElement(Children.only(children), rest);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export {default as withFragments} from './withFragments';
|
||||
export {default as withMutation} from './withMutation';
|
||||
export {default as withQuery} from './withQuery';
|
||||
|
||||
export {default as withCopyToClipboard} from './withCopyToClipboard';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Clipboard from 'clipboard';
|
||||
|
||||
export default (WrappedComponent) => {
|
||||
class WithCopyToClipboard extends React.Component {
|
||||
componentDidMount() {
|
||||
const clipboard = new Clipboard(ReactDOM.findDOMNode(this));
|
||||
|
||||
clipboard.on('success', (e) => {
|
||||
e.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {copyTarget = '', copyText = '', className = '', ...rest} = this.props;
|
||||
|
||||
return <WrappedComponent
|
||||
className={className}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-text={copyText}
|
||||
data-clipboard-target={copyTarget}
|
||||
{...rest}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithCopyToClipboard;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ class ResponseError {
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply mutation options registered in the graphRegistry.
|
||||
*/
|
||||
export default (document, config) => (WrappedComponent) => {
|
||||
export default (document, config = {}) => (WrappedComponent) => {
|
||||
config = {
|
||||
...config,
|
||||
options: config.options || {},
|
||||
|
||||
@@ -14,7 +14,7 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply query options registered in the graphRegistry.
|
||||
*/
|
||||
export default (document, config) => (WrappedComponent) => {
|
||||
export default (document, config = {}) => (WrappedComponent) => {
|
||||
config = {
|
||||
...config,
|
||||
options: config.options || {},
|
||||
|
||||
@@ -26,7 +26,7 @@ export function resetWebsocket() {
|
||||
});
|
||||
}
|
||||
|
||||
export function getClient() {
|
||||
export function getClient(options = {}) {
|
||||
if (client) {
|
||||
return client;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export function getClient() {
|
||||
);
|
||||
|
||||
client = new ApolloClient({
|
||||
...options,
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('CommentBox', () => {
|
||||
});
|
||||
|
||||
it('should render the CommentBox appropriately', () => {
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.truthy;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.true;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
import {PopupMenu, Button} from 'coral-ui';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
class FlagButton extends Component {
|
||||
export default class FlagButton extends Component {
|
||||
|
||||
state = {
|
||||
showMenu: false,
|
||||
@@ -128,7 +128,7 @@ class FlagButton extends Component {
|
||||
this.setState({message: e.target.value});
|
||||
}
|
||||
|
||||
handleClickOutside () {
|
||||
handleClickOutside = () => {
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
@@ -138,79 +138,81 @@ class FlagButton extends Component {
|
||||
const flagged = flaggedByCurrentUser || localPost;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button
|
||||
ref={(ref) => this.flagButton = ref}
|
||||
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
|
||||
className={`${name}-button`}>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{t('reported')}</span>
|
||||
: <span className={`${name}-button-text`}>{t('report')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
{
|
||||
this.state.showMenu &&
|
||||
<div className={`${name}-popup`} ref={(ref) => this.popup = ref}>
|
||||
<PopupMenu>
|
||||
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={`${name}-container`}>
|
||||
<button
|
||||
ref={(ref) => this.flagButton = ref}
|
||||
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
|
||||
className={`${name}-button`}>
|
||||
{
|
||||
popupMenu.text &&
|
||||
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{t('reported')}</span>
|
||||
: <span className={`${name}-button-text`}>{t('report')}</span>
|
||||
}
|
||||
{
|
||||
popupMenu.options && <form className={`${name}-popup-form`}>
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
{
|
||||
this.state.showMenu &&
|
||||
<div className={`${name}-popup`} ref={(ref) => this.popup = ref}>
|
||||
<PopupMenu>
|
||||
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
|
||||
{
|
||||
popupMenu.options.map((option) =>
|
||||
<div key={option.val}>
|
||||
<input
|
||||
className={`${name}-popup-radio`}
|
||||
type="radio"
|
||||
id={option.val}
|
||||
checked={this.state[popupMenu.sets] === option.val}
|
||||
onClick={this.onPopupOptionClick(popupMenu.sets)}
|
||||
value={option.val}/>
|
||||
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
|
||||
</div>
|
||||
)
|
||||
popupMenu.text &&
|
||||
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
|
||||
}
|
||||
{
|
||||
this.state.reason && <div>
|
||||
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
|
||||
{t('flag_reason')}
|
||||
</label><br/>
|
||||
<textarea
|
||||
className={`${name}-reason-text`}
|
||||
id="message"
|
||||
rows={4}
|
||||
onChange={this.onNoteTextChange}
|
||||
value={this.state.message}/>
|
||||
</div>
|
||||
popupMenu.options && <form className={`${name}-popup-form`}>
|
||||
{
|
||||
popupMenu.options.map((option) =>
|
||||
<div key={option.val}>
|
||||
<input
|
||||
className={`${name}-popup-radio`}
|
||||
type="radio"
|
||||
id={option.val}
|
||||
checked={this.state[popupMenu.sets] === option.val}
|
||||
onClick={this.onPopupOptionClick(popupMenu.sets)}
|
||||
value={option.val}/>
|
||||
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.reason && <div>
|
||||
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
|
||||
{t('flag_reason')}
|
||||
</label><br/>
|
||||
<textarea
|
||||
className={`${name}-reason-text`}
|
||||
id="message"
|
||||
rows={4}
|
||||
onChange={this.onNoteTextChange}
|
||||
value={this.state.message}/>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
<div className={`${name}-popup-counter`}>
|
||||
{this.state.step + 1} of {getPopupMenu.length}
|
||||
<div className={`${name}-popup-counter`}>
|
||||
{this.state.step + 1} of {getPopupMenu.length}
|
||||
</div>
|
||||
{
|
||||
popupMenu.button && <Button
|
||||
className={`${name}-popup-button`}
|
||||
onClick={this.onPopupContinue}>
|
||||
{popupMenu.button}
|
||||
</Button>
|
||||
}
|
||||
</PopupMenu>
|
||||
</div>
|
||||
{
|
||||
popupMenu.button && <Button
|
||||
className={`${name}-popup-button`}
|
||||
onClick={this.onPopupContinue}>
|
||||
{popupMenu.button}
|
||||
</Button>
|
||||
}
|
||||
</PopupMenu>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default onClickOutside(FlagButton);
|
||||
|
||||
const styles = {
|
||||
flaggedIcon: {
|
||||
color: '#F00'
|
||||
|
||||
@@ -5,7 +5,7 @@ const renderer = new marked.Renderer();
|
||||
|
||||
// Set link target to `_parent` to work properly in an embed.
|
||||
renderer.link = (href, title, text) =>
|
||||
`<a target="_parent" href="${href}" title="${title}">${text}</a>`;
|
||||
`<a target="_parent" href="${href}" ${title ? `title="${title}"` : ''}>${text}</a>`;
|
||||
|
||||
marked.setOptions({renderer});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ModerationLink = (props) => props.isAdmin ? (
|
||||
<div className={styles.moderationLink}>
|
||||
<a href={`/admin/moderate/${props.assetId}`} target="_blank">
|
||||
<a href={`/admin/moderate/all/${props.assetId}`} target="_blank">
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user