Merge branch 'master' into i18n-refactor

This commit is contained in:
gaba
2017-05-15 14:02:34 -07:00
251 changed files with 4984 additions and 1874 deletions
+7
View File
@@ -20,6 +20,8 @@
"no-template-curly-in-string": [1],
"no-unsafe-negation": [1],
"array-callback-return": [1],
"arrow-parens": ["warn", "always"],
"template-curly-spacing": "warn",
"eqeqeq": [2, "smart"],
"no-eval": [2],
"no-global-assign": [2],
@@ -36,6 +38,11 @@
"no-unneeded-ternary": [1],
"object-curly-spacing": [1],
"space-infix-ops": ["error"],
"space-in-parens": ["error", "never"],
"space-unary-ops": ["error", {
"words": true,
"nonwords": false
}],
"no-const-assign": [2],
"no-duplicate-imports": [2],
"prefer-template": [1],
+2
View File
@@ -20,5 +20,7 @@ plugins/*
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
**/node_modules/*
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2016 Mozilla Foundation
Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+42 -5
View File
@@ -1,6 +1,6 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
Talk is currently in Beta! [Read more about Talk here.](https://coralproject.net/products/talk.html)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
@@ -24,10 +24,13 @@ The Talk application looks for the following configuration values either as envi
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
secure cookies.
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
available in the format: `<scheme>://<host>` without the path.
- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to
sign and verify tokens via a `HS256` algorithm.
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
SMTP provider.
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
@@ -42,9 +45,43 @@ available in the format: `<scheme>://<host>` without the path.
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
### License
## Supported Browsers
Copyright 2016 Mozilla Foundation
### Web
- Chrome: latest 2 versions
- Firefox: latest 2 versions, and most recent extended support version, if any
- Safari: latest 2 versions
- Internet Explorer: IE Edge, 11
### iOS Devices
- iPad
- iPad Pro
- iPhone 6 Plus
- iPhone 6
- iPhone 5
### iOS Browsers
- Chrome for iOS: latest version
- Firefox for iOS: latest version
- Safari for iOS: latest version
### Android Devices
- Galaxy S5
- Nexus 5X
- Nexus 6P
### Android Browsers
- Chrome for Android: latest version
- Firefox for Android: latest version
## License
Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+5 -32
View File
@@ -3,12 +3,11 @@ const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const authentication = require('./middleware/authentication');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const enabled = require('debug').enabled;
const csrf = require('csurf');
const errors = require('./errors');
const session = require('./services/session');
const {createGraphOptions} = require('./graph');
const apollo = require('graphql-server-express');
@@ -37,12 +36,6 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
app.use(session);
//==============================================================================
// PASSPORT MIDDLEWARE
//==============================================================================
@@ -60,7 +53,10 @@ plugins.get('server', 'passport').forEach((plugin) => {
// Setup the PassportJS Middleware.
app.use(passport.initialize());
app.use(passport.session());
// Attach the authentication middleware, this will be responsible for decoding
// (if present) the JWT on the request.
app.use('/api', authentication);
//==============================================================================
// GraphQL Router
@@ -84,29 +80,6 @@ if (app.get('env') !== 'production') {
}
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
if (process.env.TEST_MODE === 'unit') {
// Add this fake test token in the event we are in unit test mode, and don't
// include the CSRF protection.
app.locals.csrfToken = 'UNIT_TESTS';
} else {
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
app.use(csrf({}));
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
}
//==============================================================================
// ROUTES
//==============================================================================
+4 -2
View File
@@ -9,13 +9,15 @@ const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('./util');
const {createSubscriptionManager} = require('../graph/subscriptions');
const {
PORT
} = require('../config');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.TALK_PORT || '3000');
const port = normalizePort(PORT);
app.set('port', port);
/**
-5
View File
@@ -3,11 +3,6 @@ const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
// entrypoint for the entire application.
require('env-rewrite').rewrite();
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
import {Router, Route, IndexRedirect, browserHistory} from 'react-router';
import Stories from 'containers/Stories/Stories';
import Configure from 'containers/Configure/Configure';
@@ -18,7 +18,7 @@ const routes = (
<div>
<Route exact path="/admin/install" component={InstallContainer}/>
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={Dashboard} />
<IndexRedirect to='/admin/moderate/all' />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
+3 -3
View File
@@ -24,7 +24,7 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
assets: result,
count
}))
.catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error}));
.catch((error) => dispatch({type: FETCH_ASSETS_FAILURE, error}));
};
// Update an asset state
@@ -34,9 +34,9 @@ export const updateAssetState = (id, closedAt) => (dispatch) => {
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}));
.catch((error) => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
};
export const updateAssets = assets => dispatch => {
export const updateAssets = (assets) => (dispatch) => {
dispatch({type: UPDATE_ASSETS, assets});
};
+54 -33
View File
@@ -1,75 +1,96 @@
import * as actions from '../constants/auth';
import * as Storage from 'coral-framework/helpers/storage';
import coralApi from 'coral-framework/helpers/response';
import {handleAuthToken} from 'coral-framework/actions/auth';
// Log In.
export const handleLogin = (email, password, recaptchaResponse) => dispatch => {
//==============================================================================
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
dispatch({type: actions.LOGIN_REQUEST});
const params = {method: 'POST', body: {email, password}};
if (recaptchaResponse) {
params.headers = {'X-Recaptcha-Response': recaptchaResponse};
}
return coralApi('/auth/local', params)
.then(({user}) => {
.then(({user, token}) => {
if (!user) {
Storage.removeItem('token');
return dispatch(checkLoginFailure('not logged in'));
}
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(handleAuthToken(token));
const isAdmin = !!user.roles.filter((i) => i === 'ADMIN').length;
dispatch(checkLoginSuccess(user, isAdmin));
})
.catch(error => {
.catch((error) => {
if (error.translation_key === 'login_maximum_exceeded') {
dispatch({type: actions.LOGIN_MAXIMUM_EXCEEDED, message: error.translation_key});
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: error.translation_key
});
} else {
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
}
});
};
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
export const requestPasswordReset = email => dispatch => {
const forgotPassowordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
});
const forgotPassowordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPassowordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
});
export const requestPasswordReset = (email) => (dispatch) => {
dispatch(forgotPassowordRequest(email));
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
.catch((error) => dispatch(forgotPassowordFailure(error)));
};
// Check Login
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST
});
export const checkLogin = () => dispatch => {
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
});
const checkLoginFailure = (error) => ({
type: actions.CHECK_LOGIN_FAILURE,
error
});
export const checkLogin = () => (dispatch) => {
dispatch(checkLoginRequest());
return coralApi('/auth')
.then(({user}) => {
if (!user) {
Storage.removeItem('token');
return dispatch(checkLoginFailure('not logged in'));
}
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
const isAdmin = !!user.roles.filter((i) => i === 'ADMIN').length;
dispatch(checkLoginSuccess(user, isAdmin));
})
.catch(error => {
.catch((error) => {
console.error(error);
dispatch(checkLoginFailure(`${error.translation_key}`));
});
};
// LogOut Actions
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
.then(() => dispatch(logOutSuccess()))
.catch(error => dispatch(logOutFailure(error)));
};
+3 -3
View File
@@ -16,7 +16,7 @@ import {
import coralApi from '../../../coral-framework/helpers/response';
export const fetchAccounts = (query = {}) => dispatch => {
export const fetchAccounts = (query = {}) => (dispatch) => {
dispatch(requestFetchAccounts());
coralApi(`/users?${qs.stringify(query)}`)
@@ -30,14 +30,14 @@ export const fetchAccounts = (query = {}) => dispatch => {
totalPages
});
})
.catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
.catch((error) => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
};
const requestFetchAccounts = () => ({
type: FETCH_COMMENTERS_REQUEST
});
export const updateSorting = sort => ({
export const updateSorting = (sort) => ({
type: SORT_UPDATE,
sort
});
+1 -1
View File
@@ -1,6 +1,6 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => dispatch => {
export const fetchConfig = () => (dispatch) => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({type: CONFIG_UPDATED, data});
+11 -11
View File
@@ -5,14 +5,14 @@ import errorMsj from 'coral-framework/helpers/error';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
export const goToStep = step => ({type: actions.GO_TO_STEP, step});
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
const installRequest = () => ({type: actions.INSTALL_REQUEST});
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
const installFailure = error => ({type: actions.INSTALL_FAILURE, error});
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
const hasError = error => ({type: actions.HAS_ERROR, error});
const hasError = (error) => ({type: actions.HAS_ERROR, error});
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const validation = (formData, dispatch, next) => {
@@ -21,11 +21,11 @@ const validation = (formData, dispatch, next) => {
}
const validKeys = Object.keys(formData)
.filter(name => name !== 'domains');
.filter((name) => name !== 'domains');
// Required Validation
const empty = validKeys
.filter(name => {
.filter((name) => {
const cond = !formData[name].length;
if (cond) {
@@ -45,7 +45,7 @@ const validation = (formData, dispatch, next) => {
// RegExp Validation
const validation = validKeys
.filter(name => {
.filter((name) => {
const cond = !validate[name](formData[name]);
if (cond) {
@@ -88,7 +88,7 @@ export const finishInstall = () => (dispatch, getState) => {
dispatch(installSuccess());
dispatch(nextStep());
})
.catch(error => {
.catch((error) => {
console.error(error);
dispatch(installFailure(`${error.translation_key}`));
});
@@ -99,10 +99,10 @@ export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDA
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = error => ({type: actions.CHECK_INSTALL_FAILURE, error});
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
export const checkInstall = next => dispatch => {
export const checkInstall = (next) => (dispatch) => {
dispatch(checkInstallRequest());
coralApi('/setup')
.then(({installed}) => {
@@ -111,7 +111,7 @@ export const checkInstall = next => dispatch => {
next();
}
})
.catch(error => {
.catch((error) => {
console.error(error);
dispatch(checkInstallFailure(`${error.translation_key}`));
});
+2 -2
View File
@@ -1,10 +1,10 @@
import * as actions from 'constants/moderation';
export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open});
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
// Ban User Dialog
export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote});
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
// hide shortcuts note
+5 -5
View File
@@ -13,19 +13,19 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
export const fetchSettings = () => dispatch => {
export const fetchSettings = () => (dispatch) => {
dispatch({type: SETTINGS_LOADING});
coralApi('/settings')
.then(settings => {
.then((settings) => {
dispatch({type: SETTINGS_RECEIVED, settings});
})
.catch(error => {
.catch((error) => {
dispatch({type: SETTINGS_FETCH_ERROR, error});
});
};
// for updating top-level settings
export const updateSettings = settings => {
export const updateSettings = (settings) => {
return {type: SETTINGS_UPDATED, settings};
};
@@ -48,7 +48,7 @@ export const saveSettingsToServer = () => (dispatch, getState) => {
.then(() => {
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
})
.catch(error => {
.catch((error) => {
dispatch({type: SAVE_SETTINGS_FAILED, error});
});
};
+4 -4
View File
@@ -9,8 +9,8 @@ export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch) => {
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}));
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
};
};
@@ -18,7 +18,7 @@ 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) => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
};
};
@@ -26,6 +26,6 @@ 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) => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
};
};
@@ -13,7 +13,7 @@ class AdminLogin extends React.Component {
this.state = {email: '', password: '', requestPassword: false};
}
handleSignIn = e => {
handleSignIn = (e) => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
@@ -27,7 +27,7 @@ class AdminLogin extends React.Component {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
handleRequestPassword = e => {
handleRequestPassword = (e) => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
@@ -40,11 +40,11 @@ class AdminLogin extends React.Component {
<TextField
label='Email Address'
value={this.state.email}
onChange={e => this.setState({email: e.target.value})} />
onChange={(e) => this.setState({email: e.target.value})} />
<TextField
label='Password'
value={this.state.password}
onChange={e => this.setState({password: e.target.value})}
onChange={(e) => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
<Button
@@ -53,7 +53,7 @@ class AdminLogin extends React.Component {
full
onClick={this.handleSignIn}>Sign In</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={e => {
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
@@ -81,7 +81,7 @@ class AdminLogin extends React.Component {
<TextField
label='Email Address'
value={this.state.email}
onChange={e => this.setState({email: e.target.value})} />
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
@@ -7,14 +7,14 @@ import Button from 'coral-ui/components/Button';
import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => {
const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => {
e.preventDefault();
handleBanUser({userId})
.then(handleClose)
.then(() => rejectComment({commentId}));
.then(() => commentStatus === 'REJECTED' ? null : rejectComment({commentId}));
};
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, showRejectedNote}) => (
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, commentStatus, showRejectedNote}) => (
<Dialog
className={styles.dialog}
id="banuserDialog"
@@ -35,7 +35,7 @@ const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, c
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, handleBanUser, rejectComment, handleClose)} raised>
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, commentStatus, handleBanUser, rejectComment, handleClose)} raised>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -1,7 +1,7 @@
import React, {PropTypes} from 'react';
import {Card} from 'coral-ui';
const EmptyCard = props => (
const EmptyCard = (props) => (
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
{props.children}
</Card>
@@ -58,8 +58,8 @@ export default class ModerationKeysModal extends React.Component {
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key }tr`}>
{Object.keys(shortcut.shortcuts).map((key) => (
<tr key={`${key}tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td>{lang.t(shortcut.shortcuts[key])}</td>
</tr>
@@ -193,10 +193,12 @@
box-shadow: none;
color: white;
background-color: #519954;
cursor: not-allowed;
}
.reject__active, .rejected__active {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
@@ -72,7 +72,7 @@ export default class ModerationList extends React.Component {
// Add key handlers. Each action has one and added j/k for moving around
bindKeyHandlers () {
const {modActions, isActive} = this.props;
modActions.filter(action => menuOptionsMap[action].key).forEach(action => {
modActions.filter((action) => menuOptionsMap[action].key).forEach((action) => {
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
});
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
+1 -5
View File
@@ -2,11 +2,7 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGOUT = 'LOGOUT';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
@@ -148,12 +148,12 @@ class CommunityContainer extends Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
community: state.community.toJS()
});
const mapDispatchToProps = dispatch => ({
fetchAccounts: query => dispatch(fetchAccounts(query)),
const mapDispatchToProps = (dispatch) => ({
fetchAccounts: (query) => dispatch(fetchAccounts(query)),
showBanUserDialog: (user) => dispatch(showBanUserDialog(user)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
showSuspendUserDialog: (user) => dispatch(showSuspendUserDialog(user)),
@@ -1,6 +1,6 @@
import React from 'react';
const CommunityLayout = props => (
const CommunityLayout = (props) => (
<div>
{props.children}
</div>
@@ -53,7 +53,7 @@ class Table extends Component {
<SelectField label={'Select me'} value={row.status || ''}
className={styles.selectField}
label={lang.t('community.status')}
onChange={status => this.onCommenterStatusChange(row.id, status)}>
onChange={(status) => this.onCommenterStatusChange(row.id, status)}>
<Option value={'ACTIVE'}>{lang.t('community.active')}</Option>
<Option value={'BANNED'}>{lang.t('community.banned')}</Option>
</SelectField>
@@ -62,7 +62,7 @@ class Table extends Component {
<SelectField label={'Select me'} value={row.roles[0] || ''}
className={styles.selectField}
label={lang.t('community.role')}
onChange={role => this.onRoleChange(row.id, role)}>
onChange={(role) => this.onRoleChange(row.id, role)}>
<Option value={''}>.</Option>
<Option value={'MODERATOR'}>{lang.t('community.moderator')}</Option>
<Option value={'ADMIN'}>{lang.t('community.admin')}</Option>
@@ -76,4 +76,4 @@ class Table extends Component {
}
}
export default connect(state => ({commenters: state.community.get('accounts')}))(Table);
export default connect((state) => ({commenters: state.community.get('accounts')}))(Table);
@@ -7,7 +7,7 @@ import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
// Render a single user for the list
const User = props => {
const User = (props) => {
const {user, modActionButtons} = props;
let userStatus = user.status;
@@ -35,7 +35,7 @@ const User = props => {
<div className={styles.flaggedByCount}>
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{lang.t('community.flags')}({ user.actions.length })</span>:
{ user.action_summaries.map(
(action, i ) => {
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{lang.t(`community.${action.reason}`)} ({action.count})
</span>;
@@ -44,7 +44,7 @@ const User = props => {
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i ) => {
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{lang.t(`community.${action_sum.reason}`)} ({action_sum.count})
@@ -170,7 +170,7 @@ class Configure extends Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
settings: state.settings.toJS()
});
export default connect(mapStateToProps)(Configure);
@@ -17,8 +17,8 @@ const Domainlist = ({domains, onChangeDomainlist}) => {
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
/>
</div>
</div>
@@ -30,7 +30,7 @@ class EmbedLink extends Component {
location.protocol,
'//',
location.hostname,
location.port ? (`:${ window.location.port}`) : ''
location.port ? (`:${window.location.port}`) : ''
].join('');
const coralJsUrl = [talkBaseUrl, '/embed.js'].join('');
const nonce = String(Math.random()).slice(2);
@@ -170,7 +170,7 @@ export default StreamSettings;
// To see if we are talking about weeks, days or hours
// We talk the remainder of the division and see if it's 0
const getTimeoutMeasure = ts => {
const getTimeoutMeasure = (ts) => {
if (ts % TIMESTAMPS['weeks'] === 0) {
return 'weeks';
} else if (ts % TIMESTAMPS['days'] === 0) {
@@ -182,6 +182,6 @@ const getTimeoutMeasure = ts => {
// Dividing the amount by it's measure (hours, days, weeks) we
// obtain the amount of time
const getTimeoutAmount = ts => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
const getTimeoutAmount = (ts) => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
const lang = new I18n();
@@ -15,8 +15,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => onChangeWordlist('banned', tags)}
/>
</div>
</Card>
@@ -28,8 +28,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('suspect', tags)} />
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => onChangeWordlist('suspect', tags)} />
</div>
</Card>
</div>
@@ -16,7 +16,7 @@ const ActivityWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map(asset => {
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
@@ -6,7 +6,6 @@ import {getMetrics} from 'coral-admin/src/graphql/queries';
import FlagWidget from './FlagWidget';
import ActivityWidget from './ActivityWidget';
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
import {Spinner} from 'coral-ui';
@@ -36,19 +35,14 @@ class Dashboard extends React.Component {
}
}
const mapStateToProps = state => {
const mapStateToProps = (state) => {
return {
settings: state.settings.toJS(),
moderation: state.moderation.toJS()
};
};
const mapDispatchToProps = dispatch => ({
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(mapStateToProps),
getMetrics
)(Dashboard);
@@ -17,10 +17,10 @@ const FlagWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map(asset => {
? assets.map((asset) => {
let flagSummary = null;
if (asset.action_summaries) {
flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
}
return (
@@ -17,8 +17,8 @@ const LikeWidget = ({assets}) => {
<div className={styles.widgetTable}>
{
assets.length
? assets.map(asset => {
const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
? assets.map((asset) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
@@ -7,7 +7,7 @@ import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
const lang = new I18n();
const MostLikedCommentsWidget = props => {
const MostLikedCommentsWidget = (props) => {
const {
comments,
moderation,
@@ -64,32 +64,32 @@ InstallContainer.contextTypes = {
router: React.PropTypes.object
};
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
install: state.install.toJS()
});
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch) => ({
nextStep: () => dispatch(nextStep()),
goToStep: step => dispatch(goToStep(step)),
goToStep: (step) => dispatch(goToStep(step)),
previousStep: () => dispatch(previousStep()),
finishInstall: () => dispatch(finishInstall()),
checkInstall: next => dispatch(checkInstall(next)),
handleDomainsChange: value => {
checkInstall: (next) => dispatch(checkInstall(next)),
handleDomainsChange: (value) => {
dispatch(updatePermittedDomains(value));
},
handleSettingsChange: e => {
handleSettingsChange: (e) => {
const {name, value} = e.currentTarget;
dispatch(updateSettingsFormData(name, value));
},
handleUserChange: e => {
handleUserChange: (e) => {
const {name, value} = e.currentTarget;
dispatch(updateUserFormData(name, value));
},
handleSettingsSubmit: e => {
handleSettingsSubmit: (e) => {
e.preventDefault();
dispatch(submitSettings());
},
handleUserSubmit: e => {
handleUserSubmit: (e) => {
e.preventDefault();
dispatch(submitUser());
}
@@ -6,7 +6,7 @@ const lang = new I18n();
import I18n from 'coral-i18n/modules/i18n/i18n';
const AddOrganizationName = props => {
const AddOrganizationName = (props) => {
const {handleSettingsChange, handleSettingsSubmit, install} = props;
return (
<div className={styles.step}>
@@ -6,7 +6,7 @@ const lang = new I18n();
import I18n from 'coral-i18n/modules/i18n/i18n';
const InitialStep = props => {
const InitialStep = (props) => {
const {handleUserChange, handleUserSubmit, install} = props;
return (
<div className={styles.step}>
@@ -6,7 +6,7 @@ const lang = new I18n();
import I18n from 'coral-i18n/modules/i18n/i18n';
const InitialStep = props => {
const InitialStep = (props) => {
const {nextStep} = props;
return (
<div className={styles.step}>
@@ -2,7 +2,7 @@ import React from 'react';
import styles from './style.css';
import {Button, Select, Option, TextField} from 'coral-ui';
const InviteTeamMembers = props => {
const InviteTeamMembers = (props) => {
const {nextStep} = props;
return (
<div className={styles.step}>
@@ -7,7 +7,7 @@ const lang = new I18n();
import I18n from 'coral-i18n/modules/i18n/i18n';
const PermittedDomainsStep = props => {
const PermittedDomainsStep = (props) => {
const {finishInstall, install, handleDomainsChange} = props;
const domains = install.data.settings.domains.whitelist;
return (
@@ -19,8 +19,8 @@ const PermittedDomainsStep = props => {
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => handleDomainsChange(tags)}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
onChange={(tags) => handleDomainsChange(tags)}
/>
</Card>
<Button cStyle='green' onClick={finishInstall} raised>{lang.t('PERMITTED_DOMAINS.SUBMIT')}</Button>
@@ -1,20 +1,21 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import Layout from '../components/ui/Layout';
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {fetchConfig} from '../actions/config';
import {FullLoading} from '../components/FullLoading';
import AdminLogin from '../components/AdminLogin';
import {logout} from 'coral-framework/actions/auth';
import {FullLoading} from '../components/FullLoading';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
class LayoutContainer extends Component {
componentWillMount () {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
checkLogin();
fetchConfig();
}
render () {
render() {
const {
isAdmin,
loggedIn,
@@ -24,39 +25,54 @@ class LayoutContainer extends Component {
passwordRequestSuccess
} = this.props.auth;
const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props;
if (loadingUser) { return <FullLoading />; }
const {
handleLogout,
toggleShortcutModal,
TALK_RECAPTCHA_PUBLIC
} = this.props;
if (loadingUser) {
return <FullLoading />;
}
if (!isAdmin) {
return <AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError} />;
return (
<AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError}
/>
);
}
if (isAdmin && loggedIn) {
return <Layout handleLogout={handleLogout} toggleShortcutModal={toggleShortcutModal} {...this.props} />;
return (
<Layout
handleLogout={handleLogout}
toggleShortcutModal={toggleShortcutModal}
{...this.props}
/>
);
}
return <FullLoading />;
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
TALK_RECAPTCHA_PUBLIC: state.config.get('data').get('TALK_RECAPTCHA_PUBLIC', null)
TALK_RECAPTCHA_PUBLIC: state.config
.get('data')
.get('TALK_RECAPTCHA_PUBLIC', null)
});
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch) => ({
checkLogin: () => dispatch(checkLogin()),
fetchConfig: () => dispatch(fetchConfig()),
handleLogin: (username, password, recaptchaResponse) => dispatch(handleLogin(username, password, recaptchaResponse)),
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
toggleShortcutModal: toggle => dispatch(toggleShortcutModal(toggle)),
handleLogin: (username, password, recaptchaResponse) =>
dispatch(handleLogin(username, password, recaptchaResponse)),
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
handleLogout: () => dispatch(logout())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(LayoutContainer);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -43,12 +43,13 @@ class ModerationContainer extends Component {
const {acceptComment, rejectComment} = this.props;
const {selectedIndex} = this.state;
const comments = this.getComments();
const commentId = {commentId: comments[selectedIndex].id};
const comment = comments[selectedIndex];
const commentId = {commentId: comment.id};
if (accept) {
acceptComment(commentId);
comment.status !== 'ACCEPTED' && acceptComment(commentId);
} else {
rejectComment(commentId);
comment.status !== 'REJECTED' && rejectComment(commentId);
}
}
@@ -60,14 +61,14 @@ class ModerationContainer extends Component {
select = (next) => () => {
if (next) {
this.setState(prevState =>
this.setState((prevState) =>
({
...prevState,
selectedIndex: prevState.selectedIndex < this.getComments().length - 1
? prevState.selectedIndex + 1 : prevState.selectedIndex
}));
} else {
this.setState(prevState =>
this.setState((prevState) =>
({
...prevState,
selectedIndex: prevState.selectedIndex > 0 ?
@@ -125,7 +126,7 @@ class ModerationContainer extends Component {
}
if (providedAssetId) {
asset = assets.find(asset => asset.id === this.props.params.id);
asset = assets.find((asset) => asset.id === this.props.params.id);
if (!asset) {
return <NotFoundAsset assetId={providedAssetId} />;
@@ -185,6 +186,7 @@ class ModerationContainer extends Component {
open={moderation.banDialog}
user={moderation.user}
commentId={moderation.commentId}
commentStatus={moderation.commentStatus}
handleClose={props.hideBanUserDialog}
handleBanUser={props.banUser}
showRejectedNote={moderation.showRejectedNote}
@@ -200,19 +202,19 @@ class ModerationContainer extends Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
moderation: state.moderation.toJS(),
settings: state.settings.toJS(),
assets: state.assets.get('assets')
});
const mapDispatchToProps = dispatch => ({
toggleModal: toggle => dispatch(toggleModal(toggle)),
const mapDispatchToProps = (dispatch) => ({
toggleModal: (toggle) => dispatch(toggleModal(toggle)),
onClose: () => dispatch(toggleModal(false)),
singleView: () => dispatch(singleView()),
updateAssets: assets => dispatch(updateAssets(assets)),
updateAssets: (assets) => dispatch(updateAssets(assets)),
fetchSettings: () => dispatch(fetchSettings()),
showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)),
showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
hideShortcutsNote: () => dispatch(hideShortcutsNote()),
});
@@ -1,6 +1,6 @@
import React from 'react';
const ModerationLayout = props => (
const ModerationLayout = (props) => (
<div>
{props.children}
</div>
@@ -1,27 +1,36 @@
import React, {PropTypes} from 'react';
import timeago from 'timeago.js';
import Linkify from 'react-linkify';
import Highlighter from 'react-highlight-words';
import {Link} from 'react-router';
import Linkify from 'react-linkify';
import styles from './styles.css';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import styles from './styles.css';
import CommentType from './CommentType';
import Highlighter from 'react-highlight-words';
import Slot from 'coral-framework/components/Slot';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import BanUserButton from 'coral-admin/src/components/BanUserButton';
import {getActionSummary} from 'coral-framework/utils';
const linkify = new Linkify();
import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
const Comment = ({actions = [], comment, ...props}) => {
const Comment = ({
actions = [],
comment,
suspectWords,
bannedWords,
...props
}) => {
const links = linkify.getMatches(comment.body);
const linkText = links ? links.map(link => link.raw) : [];
const linkText = links ? links.map((link) => link.raw) : [];
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
const flagActions =
comment.actions &&
comment.actions.filter((a) => a.__typename === 'FlagAction');
let commentType = '';
if (comment.status === 'PREMOD') {
commentType = 'premod';
@@ -29,8 +38,20 @@ const Comment = ({actions = [], comment, ...props}) => {
commentType = 'flagged';
}
// since words are checked against word boundaries on the backend,
// this should be the behavior on the front end as well.
// currently the highlighter plugin does not support this out of the box.
const searchWords = [...suspectWords, ...bannedWords]
.filter((w) => {
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
})
.concat(linkText);
return (
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
<li
tabIndex={props.index}
className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}
>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
@@ -38,53 +59,95 @@ const Comment = ({actions = [], comment, ...props}) => {
{comment.user.name}
</span>
<span className={styles.created}>
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
{timeago().format(
comment.created_at || Date.now() - props.index * 60 * 1000,
lang.getLocale().replace('-', '_')
)}
</span>
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
<BanUserButton
user={comment.user}
onClick={() =>
props.showBanUserDialog(
comment.user,
comment.id,
comment.status,
comment.status !== 'REJECTED'
)}
/>
<CommentType type={commentType} />
</div>
{comment.user.status === 'banned' ?
<span className={styles.banned}>
<Icon name='error_outline'/>
{lang.t('comment.banned_user')}
</span>
{comment.user.status === 'banned'
? <span className={styles.banned}>
<Icon name="error_outline" />
{lang.t('comment.banned_user')}
</span>
: null}
<Slot fill="adminCommentInfoBar" comment={comment} />
</div>
<div className={styles.moderateArticle}>
Story: {comment.asset.title}
{!props.currentAsset && (
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate </Link>
)}
{!props.currentAsset &&
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate </Link>}
</div>
<div className={styles.itemBody}>
<p className={styles.body}>
<Highlighter
searchWords={[...props.suspectWords, ...props.bannedWords, ...linkText]}
textToHighlight={comment.body} />
searchWords={searchWords}
textToHighlight={comment.body}
/>
{' '}
<a
className={styles.external}
href={`${comment.asset.url}#${comment.id}`}
target="_blank"
>
<Icon name="open_in_new" /> {lang.t('comment.view_context')}
</a>
</p>
<Slot fill="adminCommentContent" comment={comment} />
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
{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 key={i}
type={action}
user={comment.user}
status={comment.status}
active={active}
acceptComment={() => props.acceptComment({commentId: comment.id})}
rejectComment={() => props.rejectComment({commentId: comment.id})} />;
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
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 fill="adminSideActions" comment={comment} />
</div>
</div>
</div>
{
flagActions && flagActions.length
? <FlagBox actions={flagActions} actionSummaries={flagActionSummaries} />
: null
}
<div>
<Slot fill="adminCommentDetailArea" comment={comment} />
</div>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
/>
: null}
</li>
);
};
@@ -105,6 +168,7 @@ Comment.propTypes = {
}),
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
})
})
@@ -2,7 +2,7 @@ import React, {PropTypes} from 'react';
import styles from './CommentType.css';
import {Icon} from 'coral-ui';
const CommentType = props => {
const CommentType = (props) => {
const typeData = getTypeData(props.type);
return (
@@ -12,7 +12,7 @@ const CommentType = props => {
);
};
const getTypeData = type => {
const getTypeData = (type) => {
switch (type) {
case 'premod':
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
@@ -54,7 +54,7 @@ class FlagBox extends Component {
<ul>
{actionSummaries.map((summary, i) => {
const actionList = actions.filter(a => a.reason === summary.reason);
const actionList = actions.filter((a) => a.reason === summary.reason);
return (
<li key={i}>
@@ -3,7 +3,7 @@ import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import styles from './styles.css';
const ModerationHeader = props => (
const ModerationHeader = (props) => (
<div className=''>
<div className={`mdl-tabs ${styles.header}`}>
{
@@ -27,12 +27,6 @@ const ModerationMenu = (
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {lang.t('modqueue.all')} <CommentCount count={allCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('premod')}
className={`mdl-tabs__tab ${styles.tab}`}
@@ -45,6 +39,12 @@ const ModerationMenu = (
activeClassName={styles.active}>
<Icon name='flag' className={styles.tabIcon} /> {lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}
@@ -56,7 +56,7 @@ const ModerationMenu = (
className={styles.selectField}
label="Sort"
value={sort}
onChange={sort => selectSort(sort)}>
onChange={(sort) => selectSort(sort)}>
<Option value={'REVERSE_CHRONOLOGICAL'}>Newest First</Option>
<Option value={'CHRONOLOGICAL'}>Oldest First</Option>
</SelectField>
@@ -2,7 +2,7 @@ import React from 'react';
import {Link} from 'react-router';
import styles from './styles.css';
const NotFound = props => (
const NotFound = (props) => (
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
<p>
The provided asset id <Link to={`/admin/moderate/${props.assetId}`}>{props.assetId}</Link> does not exist.
@@ -423,3 +423,24 @@ span {
position: relative;
top: 7px;
}
.external {
font-size: .7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
font-weight: normal;
margin-left: 10px;
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
}
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
@@ -54,7 +54,7 @@ class Stories extends Component {
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
if (statusMenuOpen) {
this.setState(prev => {
this.setState((prev) => {
prev.statusMenus[id] = false;
return prev;
});
@@ -64,7 +64,7 @@ class Stories extends Component {
this.props.fetchAssets(page, limit, search, sort, filter);
});
} else {
this.setState(prev => {
this.setState((prev) => {
prev.statusMenus[id] = true;
return prev;
});
@@ -54,7 +54,7 @@ class Stories extends Component {
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
if (statusMenuOpen) {
this.setState(prev => {
this.setState((prev) => {
prev.statusMenus[id] = false;
return prev;
});
@@ -64,7 +64,7 @@ class Stories extends Component {
this.props.fetchAssets(page, limit, search, sort, filter);
});
} else {
this.setState(prev => {
this.setState((prev) => {
prev.statusMenus[id] = true;
return prev;
});
@@ -11,6 +11,7 @@ fragment commentView on Comment {
asset {
id
title
url
}
action_summaries {
count
@@ -56,7 +56,7 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
updateQueries: {
ModQueue: (oldData) => {
const comment = views.reduce((comment, view) => {
return comment ? comment : oldData[view].find(c => c.id === commentId);
return comment ? comment : oldData[view].find((c) => c.id === commentId);
}, null);
let accepted;
let acceptedCount = oldData.acceptedCount;
@@ -70,9 +70,9 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
accepted = [comment, ...oldData.accepted];
}
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const rejected = oldData.rejected.filter(c => c.id !== commentId);
const premod = oldData.premod.filter((c) => c.id !== commentId);
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
const rejected = oldData.rejected.filter((c) => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
@@ -101,7 +101,7 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
updateQueries: {
ModQueue: (oldData) => {
const comment = views.reduce((comment, view) => {
return comment ? comment : oldData[view].find(c => c.id === commentId);
return comment ? comment : oldData[view].find((c) => c.id === commentId);
}, null);
let rejected;
let rejectedCount = oldData.rejectedCount;
@@ -115,9 +115,9 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
rejected = [comment, ...oldData.rejected];
}
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const accepted = oldData.accepted.filter(c => c.id !== commentId);
const premod = oldData.premod.filter((c) => c.id !== commentId);
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
const accepted = oldData.accepted.filter((c) => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
+1 -3
View File
@@ -26,10 +26,8 @@ export default function auth (state = initialState, action) {
.set('loadingUser', false)
.set('isAdmin', action.isAdmin)
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
case actions.LOGOUT:
return initialState;
case actions.LOGIN_REQUEST:
return state.set('loginError', null);
case actions.LOGIN_SUCCESS:
return state.set('loginMaxExceeded', false).set('loginError', null);
case actions.LOGIN_FAILURE:
+4 -4
View File
@@ -53,17 +53,17 @@ export default function community (state = initialState, action) {
}
case SET_ROLE : {
const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
const idx = commenters.findIndex((el) => el.id === action.id);
commenters[idx].roles[0] = action.role;
return state.set('accounts', commenters.map(id => id));
return state.set('accounts', commenters.map((id) => id));
}
case SET_COMMENTER_STATUS: {
const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
const idx = commenters.findIndex((el) => el.id === action.id);
commenters[idx].status = action.status;
return state.set('accounts', commenters.map(id => id));
return state.set('accounts', commenters.map((id) => id));
}
case SORT_UPDATE :
@@ -6,6 +6,7 @@ const initialState = Map({
modalOpen: false,
user: Map({}),
commentId: null,
commentStatus: null,
banDialog: false,
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show'
});
@@ -14,12 +15,14 @@ export default function moderation (state = initialState, action) {
switch (action.type) {
case actions.HIDE_BANUSER_DIALOG:
return state
.set('banDialog', false);
.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
});
+2 -2
View File
@@ -1,5 +1,5 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
import {networkInterface} from 'coral-framework/services/transport';
import fragmentMatcher from './fragmentMatcher';
export const client = new ApolloClient({
@@ -12,5 +12,5 @@ export const client = new ApolloClient({
}
return null;
},
networkInterface: getNetworkInterface()
networkInterface
});
@@ -39,7 +39,7 @@ const fm = new IntrospectionFragmentMatcher({
{name: 'DefaultAction'},
{name: 'FlagAction'},
{name: 'DontAgreeAction'}
],
]
},
{
kind: 'INTERFACE',
@@ -48,18 +48,18 @@ const fm = new IntrospectionFragmentMatcher({
{name: 'DefaultActionSummary'},
{name: 'FlagActionSummary'},
{name: 'DontAgreeActionSummary'}
],
]
},
{
kind: 'INTERFACE',
name: 'AssetActionSummary',
possibleTypes: [
{name: 'DefaultAssetActionSummary'},
{name: 'FlagAssetActionSummary'},
{name: 'FlagAssetActionSummary'}
]
}
],
},
]
}
}
});
@@ -1,11 +0,0 @@
import {createNetworkInterface} from 'apollo-client';
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
return new createNetworkInterface({
uri: apiUrl,
opts: {
credentials: 'same-origin',
headers,
},
});
}
+2
View File
@@ -72,6 +72,7 @@
"flagged": "flagged",
"anon": "Anonymous",
"ban_user": "Ban User",
"view_context": "View context",
"banned_user": "Banned User"
},
"user": {
@@ -259,6 +260,7 @@
"comment": {
"flagged": "marcado",
"anon": "Anónimo",
"view_context": "Ver contexto",
"ban_user": "Suspender Usuario",
"banned_user": "Usuario Suspendido"
},
@@ -125,9 +125,9 @@ const mapStateToProps = (state) => ({
asset: state.asset.toJS()
});
const mapDispatchToProps = dispatch => ({
updateStatus: status => dispatch(updateOpenStatus(status)),
updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig)),
const mapDispatchToProps = (dispatch) => ({
updateStatus: (status) => dispatch(updateOpenStatus(status)),
updateConfiguration: (newConfig) => dispatch(updateConfiguration(newConfig)),
});
export default compose(
@@ -0,0 +1,6 @@
import {ADD_EXTERNAL_CONFIG} from '../constants/config';
export const addExternalConfig = (config) => ({
type: ADD_EXTERNAL_CONFIG,
config
});
@@ -7,4 +7,3 @@ export const setActiveTab = (tab) => (dispatch, getState) => {
dispatch(viewAllComments());
}
};
@@ -13,13 +13,87 @@
pointer-events: none;
}
.topRightMenu {
.bylineSecondary {
color: #696969;
font-size: 12px;
}
.editedMarker {
font-style: italic;
}
/* element in the top right of the Comment */
.topRight {
float: right;
margin-top: 10px;
text-align: right;
}
.topRight > * {
text-align: initial;
}
.topRight .popover {
margin-top: 1em;
right: 0px;
}
.topRight .link.active,
.topRight .active .link {
padding-bottom: 0.125em;
border-bottom: 2px solid currentColor;
}
.topRightMenu {
cursor: pointer;
margin-top: 5px;
}
.topRightMenu > * {
text-align: initial;
.editCommentForm {
margin-bottom: 10px;
}
.editCommentForm .buttonContainerLeft {
margin-right: auto;
display: flex;
justify-content: center;
flex-direction: column;
}
.editCommentForm .buttonContainerLeft .editWindowRemaining {
margin-right: 1em;
}
.editCommentForm .button {
flex-shrink: 0;
}
.editWindowAlmostOver {
font-weight: bold;
}
.link {
color: #2376D8;
cursor: pointer;
}
.popover {
position: absolute;
z-index: 1;
}
/* Wizard used for Ignore User, Delete Comment confirmations */
.Wizard {
background-color: #2E343B;
color: white;
padding: 1em;
max-width: 220px; /* consider moving to better class */
}
.Wizard header {
font-weight: bold;
}
.Wizard .textAlignRight {
text-align: right;
}
@@ -19,11 +19,13 @@ import Slot from 'coral-framework/components/Slot';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import classnames from 'classnames';
import {EditableCommentContent} from './EditableCommentContent';
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
import {getEditableUntilDate} from './util';
import styles from './Comment.css';
const isStaff = tags => !tags.every(t => t.name !== 'STAFF');
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF');
// hold actions links (e.g. Reply) along the comment footer
const ActionButton = ({children}) => {
@@ -37,7 +39,17 @@ const ActionButton = ({children}) => {
class Comment extends React.Component {
constructor(props) {
super(props);
this.state = {replyBoxVisible: false};
// timeout to keep track of Comment edit window expiration
this.editWindowExpiryTimeout = null;
this.onClickEdit = this.onClickEdit.bind(this);
this.stopEditing = this.stopEditing.bind(this);
this.state = {
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
isEditing: false,
replyBoxVisible: false,
};
}
static propTypes = {
@@ -53,7 +65,7 @@ class Comment extends React.Component {
parentId: PropTypes.string,
highlighted: PropTypes.string,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
@@ -84,7 +96,13 @@ class Comment extends React.Component {
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired,
editing: PropTypes.shape({
edited: PropTypes.bool,
// ISO8601
editableUntil: PropTypes.string,
})
}).isRequired,
// given a comment, return whether it should be rendered as ignored
@@ -97,17 +115,53 @@ class Comment extends React.Component {
removeCommentTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func
};
ignoreUser: React.PropTypes.func,
render() {
// edit a comment, passed (id, asset_id, { body })
editComment: React.PropTypes.func,
}
onClickEdit (e) {
e.preventDefault();
this.setState({isEditing: true});
}
stopEditing () {
if (this._isMounted) {
this.setState({isEditing: false});
}
}
componentDidMount() {
this._isMounted = true;
if (this.editWindowExpiryTimeout) {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
// if still in the edit window, set a timeout to re-render once it expires
const msLeftToEdit = editWindowRemainingMs(this.props.comment);
if (msLeftToEdit > 0) {
this.editWindowExpiryTimeout = setTimeout(() => {
// re-render
this.setState(this.state);
}, msLeftToEdit);
}
}
componentWillUnmount() {
if (this.editWindowExpiryTimeout) {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
this._isMounted = false;
}
render () {
const {
comment,
parentId,
currentUser,
asset,
depth,
postItem,
postComment,
addNotification,
showSignInDialog,
highlighted,
@@ -133,9 +187,9 @@ class Comment extends React.Component {
);
let myFlag = null;
if (iPerformedThisAction('FlagActionSummary', comment)) {
myFlag = flagSummary.find(s => s.current_user);
myFlag = flagSummary.find((s) => s.current_user);
} else if (iPerformedThisAction('DontAgreeActionSummary', comment)) {
myFlag = dontAgreeSummary.find(s => s.current_user);
myFlag = dontAgreeSummary.find((s) => s.current_user);
}
let commentClass = parentId
@@ -147,7 +201,7 @@ class Comment extends React.Component {
const notifyOnError = (fn, errorToMessage) =>
async function(...args) {
if (typeof errorToMessage !== 'function') {
errorToMessage = error => error.message;
errorToMessage = (error) => error.message;
}
try {
return await fn(...args);
@@ -190,8 +244,17 @@ class Comment extends React.Component {
{commentIsBest(comment)
? <TagLabel><BestIndicator /></TagLabel>
: null}
<PubDate created_at={comment.created_at} />
: null }
<span className={styles.bylineSecondary}>
<PubDate created_at={comment.created_at} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>(Edited)</span></span>
: null
}
</span>
<Slot
fill="commentInfoBar"
data={this.props.data}
@@ -200,18 +263,47 @@ class Comment extends React.Component {
commentId={comment.id}
inline
/>
{currentUser && comment.user.id !== currentUser.id
? <span className={styles.topRightMenu}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification}
/>
</span>
: null}
<Content body={comment.body} />
<Slot fill="commentContent" />
{ (currentUser &&
(comment.user.id === currentUser.id))
/* User can edit/delete their own comment for a short window after posting */
? <span className={classnames(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={classnames(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
/* TopRightMenu allows currentUser to ignore other users' comments */
: <span className={classnames(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
}
{
this.state.isEditing
? <EditableCommentContent
editComment={this.props.editComment.bind(null, comment.id, asset.id)}
addNotification={addNotification}
asset={asset}
comment={comment}
currentUser={currentUser}
maxCharCount={maxCharCount}
parentId={parentId}
stopEditing={this.stopEditing}
/>
: <div>
<Content body={comment.body} />
<Slot fill="commentContent" />
</div>
}
<div className="commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
@@ -278,12 +370,12 @@ class Comment extends React.Component {
parentId={parentId || comment.id}
addNotification={addNotification}
authorId={currentUser.id}
postItem={postItem}
postComment={postComment}
assetId={asset.id}
/>
: null}
{comment.replies &&
comment.replies.map(reply => {
comment.replies.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
@@ -294,7 +386,8 @@ class Comment extends React.Component {
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
postComment={postComment}
editComment={this.props.editComment}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
@@ -330,3 +423,21 @@ class Comment extends React.Component {
}
export default Comment;
// return whether the comment is editable
function commentIsStillEditable (comment) {
const editing = comment && comment.editing;
if (!editing) {return false;}
const editableUntil = getEditableUntilDate(comment);
const editWindowExpired = (editableUntil - new Date) < 0;
return !editWindowExpired;
}
// return number of milliseconds before edit window expires
function editWindowRemainingMs (comment) {
const editableUntil = getEditableUntilDate(comment);
if (!editableUntil) {return;}
const now = new Date();
const editWindowRemainingMs = (editableUntil - now);
return editWindowRemainingMs;
}
@@ -0,0 +1,53 @@
import React, {PropTypes} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
/**
* Countdown the number of seconds until a given Date
*/
export class CountdownSeconds extends React.Component {
static propTypes = {
until: PropTypes.instanceOf(Date).isRequired,
classNameForMsRemaining: PropTypes.func,
}
constructor(props) {
super(props);
this.countdownInterval = null;
}
componentDidMount() {
const {until} = this.props;
const now = new Date();
if (until - now > 0) {
this.countdownInterval = setInterval(() => {
// re-render
this.forceUpdate();
}, 1000);
}
}
componentWillUnmount() {
if (this.countdownInterval) {
this.countdownInterval = clearInterval(this.countdownInterval);
}
}
render() {
const now = new Date();
const {until, classNameForMsRemaining} = this.props;
const msRemaining = until - now;
const secRemaining = msRemaining / 1000;
const wholeSecRemaining = Math.floor(secRemaining);
const plural = secRemaining !== 1;
const units = lang.t(plural ? 'editComment.secondsPlural' : 'editComment.second');
let classFromProp;
if (typeof classNameForMsRemaining === 'function') {
classFromProp = classNameForMsRemaining(msRemaining);
}
return (
<span className={classFromProp}>
{`${wholeSecRemaining} ${units}`}
</span>
);
}
}
@@ -0,0 +1,153 @@
import React, {PropTypes} from 'react';
import {notifyForNewCommentStatus} from 'coral-plugin-commentbox/CommentBox';
import {CommentForm} from 'coral-plugin-commentbox/CommentForm';
import styles from './Comment.css';
import {CountdownSeconds} from './CountdownSeconds';
import {getEditableUntilDate} from './util';
import {Icon} from 'coral-ui';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
/**
* Renders a Comment's body in such a way that the end-user can edit it and save changes
*/
export class EditableCommentContent extends React.Component {
static propTypes = {
// show notification to the user (e.g. for errors)
addNotification: PropTypes.func.isRequired,
asset: PropTypes.shape({
settings: PropTypes.shape({
charCountEnable: PropTypes.bool,
}),
}).isRequired,
// comment that is being edited
comment: PropTypes.shape({
body: PropTypes.string,
editing: PropTypes.shape({
edited: PropTypes.bool,
// ISO8601
editableUntil: PropTypes.string,
})
}).isRequired,
// logged in user
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
maxCharCount: PropTypes.number,
// edit a comment, passed {{ body }}
editComment: React.PropTypes.func,
// called when editing should be stopped
stopEditing: React.PropTypes.func,
}
constructor(props) {
super(props);
this.editComment = this.editComment.bind(this);
this.editWindowExpiryTimeout = null;
}
componentDidMount() {
const editableUntil = getEditableUntilDate(this.props.comment);
const now = new Date();
const editWindowRemainingMs = editableUntil && (editableUntil - now);
if (editWindowRemainingMs > 0) {
this.editWindowExpiryTimeout = setTimeout(() => {
this.forceUpdate();
}, editWindowRemainingMs);
}
}
componentWillUnmount() {
if (this.editWindowExpiryTimeout) {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
}
async editComment(edit) {
const {editComment, addNotification, stopEditing} = this.props;
if (typeof editComment !== 'function') {return;}
let response;
let successfullyEdited = false;
try {
response = await editComment(edit);
const errors = (response && response.data && response.data.editComment)
? response.data.editComment.errors
: null;
if (errors && (errors.length === 1)) {
throw errors[0];
}
successfullyEdited = true;
} catch (error) {
if (error.translation_key) {
addNotification('error', lang.t(`error.${error.translation_key}`));
} else if (error.networkError) {
addNotification('error', lang.t('error.networkError'));
} else {
addNotification('error', lang.t('editComment.unexpectedError'));
throw error;
}
}
if (successfullyEdited) {
const status = response.data.editComment.comment.status;
notifyForNewCommentStatus(this.props.addNotification, status);
}
if (successfullyEdited && typeof stopEditing === 'function') {
stopEditing();
}
}
render() {
const originalBody = this.props.comment.body;
const editableUntil = getEditableUntilDate(this.props.comment);
const editWindowExpired = (editableUntil - new Date()) < 0;
return (
<div className={styles.editCommentForm}>
<CommentForm
defaultValue={this.props.comment.body}
charCountEnable={this.props.asset.settings.charCountEnable}
maxCharCount={this.props.maxCharCount}
saveCommentEnabled={(comment) => {
// should be disabled if user hasn't actually changed their
// original comment
return (comment.body !== originalBody) && !editWindowExpired;
}}
saveComment={this.editComment}
bodyLabel={lang.t('editComment.bodyInputLabel')}
bodyPlaceholder=""
submitText={<span>{lang.t('editComment.saveButton')}</span>}
saveButtonCStyle="green"
cancelButtonClicked={this.props.stopEditing}
buttonClass={styles.button}
buttonContainerStart={
<div className={styles.buttonContainerLeft}>
<span className={styles.editWindowRemaining}>
{
editWindowExpired
? <span>
{lang.t('editComment.editWindowExpired')}
{
typeof this.props.stopEditing === 'function'
? <span>&nbsp;<a className={styles.link} onClick={this.props.stopEditing}>{lang.t('editComment.editWindowExpiredClose')}</a></span>
: null
}
</span>
: <span>
<Icon name="timer"/> {lang.t('editComment.editWindowTimerPrefix')}
<CountdownSeconds
until={editableUntil}
classNameForMsRemaining={(remainingMs) => (remainingMs <= 10 * 1000) ? styles.editWindowAlmostOver : '' }
/>
</span>
}
</span>
</div>
}
/>
</div>
);
}
}
@@ -1,14 +0,0 @@
.IgnoreUserWizard {
background-color: #2E343B;
color: white;
padding: 1em;
max-width: 220px;
}
.IgnoreUserWizard header {
font-weight: bold;
}
.IgnoreUserWizard .textAlignRight {
text-align: right;
}
@@ -1,5 +1,5 @@
import React, {PropTypes} from 'react';
import styles from './IgnoreUserWizard.css';
import styles from './Comment.css';
import {Button} from 'coral-ui';
// Guides the user through ignoring another user, including confirming their decision
@@ -58,7 +58,7 @@ export class IgnoreUserWizard extends React.Component {
const {step} = this.state;
const elForThisStep = elsForStep[step - 1];
return (
<div className={styles.IgnoreUserWizard}>
<div className={styles.Wizard}>
{ elForThisStep }
</div>
);
@@ -15,7 +15,7 @@ import ChangeUsernameContainer
from 'coral-sign-in/containers/ChangeUsernameContainer';
class Stream extends React.Component {
setActiveReplyBox = reactKey => {
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
} else {
@@ -26,7 +26,7 @@ class Stream extends React.Component {
render() {
const {
root: {asset, asset: {comments}, comment, myIgnoredUsers},
postItem,
postComment,
addNotification,
postFlag,
postDontAgree,
@@ -58,7 +58,7 @@ class Stream extends React.Component {
const firstCommentDate = asset.comments[0]
? asset.comments[0].created_at
: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const commentIsIgnored = comment =>
const commentIsIgnored = (comment) =>
myIgnoredUsers && myIgnoredUsers.includes(comment.user.id);
return (
<div id="stream">
@@ -84,7 +84,7 @@ class Stream extends React.Component {
{user
? <CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
postComment={this.props.postComment}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
setCommentCountCache={this.props.setCommentCountCache}
@@ -122,7 +122,7 @@ class Stream extends React.Component {
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={this.props.postItem}
postComment={this.props.postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
@@ -137,6 +137,7 @@ class Stream extends React.Component {
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
/>
: <div>
<NewCount
@@ -149,7 +150,7 @@ class Stream extends React.Component {
/>
<div className="embed__stream">
{comments.map(
comment =>
(comment) =>
(commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
@@ -160,7 +161,7 @@ class Stream extends React.Component {
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
@@ -178,6 +179,7 @@ class Stream extends React.Component {
pluginProps={pluginProps}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
/>)
)}
</div>
@@ -196,7 +198,7 @@ class Stream extends React.Component {
Stream.propTypes = {
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
@@ -205,7 +207,10 @@ Stream.propTypes = {
removeCommentTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func
ignoreUser: React.PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: React.PropTypes.func,
};
export default Stream;
@@ -20,5 +20,4 @@
position: relative;
transform: rotate(180deg);
top: 0;
/*top: -0.25em;*/
}
@@ -72,7 +72,7 @@ class Toggleable extends React.Component {
};
}
toggle() {
this.setState({isOpen: ! this.state.isOpen});
this.setState({isOpen: !this.state.isOpen});
}
close() {
this.setState({isOpen: false});
@@ -0,0 +1,10 @@
/**
* Given a comment, return when the comment can no longer be edited
* @param {Object} comment
* @returns {Date} when the comment can no longer be edited.
*/
export const getEditableUntilDate = (comment) => {
const editing = comment && comment.editing;
const editableUntil = editing && editing.editableUntil && new Date(Date.parse(editing.editableUntil));
return editableUntil;
};
@@ -0,0 +1 @@
export const ADD_EXTERNAL_CONFIG = 'ADD_EXTERNAL_CONFIG';
@@ -1,6 +1,6 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import withFragments from 'coral-framework/hocs/withFragments';
import {withFragments} from 'coral-framework/hocs';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
const pluginFragments = getSlotsFragments([
@@ -41,6 +41,10 @@ export default withFragments({
id
}
}
editing {
edited
editableUntil
}
${pluginFragments.spreads('comment')}
}
${pluginFragments.definitions('comment')}
@@ -1,5 +1,5 @@
import React from 'react';
import {compose, gql, graphql} from 'react-apollo';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import isEqual from 'lodash/isEqual';
@@ -8,31 +8,28 @@ import renderComponent from 'recompose/renderComponent';
import {Spinner} from 'coral-ui';
import {authActions, assetActions, pym} from 'coral-framework';
import {getDefinitionName, separateDataAndRoot} from 'coral-framework/utils';
import {getDefinitionName} from 'coral-framework/utils';
import {withQuery} from 'coral-framework/hocs';
import Embed from '../components/Embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {setActiveTab} from '../actions/embed';
import Stream from './Stream';
import {setActiveTab} from '../actions/embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
componentDidMount() {
pym.sendMessage('childReady');
}
componentWillReceiveProps(nextProps) {
if(this.props.root.me && !nextProps.root.me) {
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
// Refetch because on logout `excludeIgnored` becomes `false`.
// TODO: logout via mutation and obsolete this?
// Refetch after login/logout.
this.props.data.refetch();
}
const {fetchAssetSuccess} = this.props;
if(!isEqual(nextProps.root.asset, this.props.root.asset)) {
if (!isEqual(nextProps.root.asset, this.props.root.asset)) {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.root.asset);
@@ -47,7 +44,7 @@ class EmbedContainer extends React.Component {
}
componentDidUpdate(prevProps) {
if(!isEqual(prevProps.root.comment, this.props.root.comment)) {
if (!isEqual(prevProps.root.comment, this.props.root.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
@@ -75,7 +72,7 @@ const EMBED_QUERY = gql`
${Stream.fragments.root}
`;
export const withQuery = graphql(EMBED_QUERY, {
export const withEmbedQuery = withQuery(EMBED_QUERY, {
options: ({auth, commentId, assetId, assetUrl}) => ({
variables: {
assetId,
@@ -85,34 +82,33 @@ export const withQuery = graphql(EMBED_QUERY, {
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
},
}),
props: ({data}) => separateDataAndRoot(data),
});
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
config: state.config
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
fetchAssetSuccess,
checkLogin,
setCommentCountCache,
viewAllComments,
logout,
setActiveTab,
}, dispatch);
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
logout,
checkLogin,
setActiveTab,
viewAllComments,
fetchAssetSuccess,
setCommentCountCache
},
dispatch
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(
props => !props.auth.checkedInitialLogin,
renderComponent(Spinner),
),
withQuery,
branch((props) => !props.auth.checkedInitialLogin && props.config, renderComponent(Spinner)),
withEmbedQuery,
)(EmbedContainer);
@@ -6,13 +6,17 @@ import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
import {postComment, postFlag, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import withFragments from 'coral-framework/hocs/withFragments';
import {withFragments} from 'coral-framework/hocs';
import {getDefinitionName} from 'coral-framework/utils';
const {showSignInDialog} = authActions;
@@ -25,7 +29,7 @@ class StreamContainer extends React.Component {
variables,
// Apollo requires this, even though we don't use it...
updateQuery: data => data,
updateQuery: (data) => data,
});
};
@@ -75,7 +79,7 @@ class StreamContainer extends React.Component {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map(comment => {
comments: oldData.asset.comments.map((comment) => {
// since the dipslayed replies and the returned replies can overlap,
// pull out the unique ones.
@@ -199,10 +203,6 @@ const fragments = {
}
}
}
myIgnoredUsers {
id,
username,
}
me {
status
}
@@ -213,7 +213,7 @@ const fragments = {
`,
};
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
commentCountCache: state.stream.commentCountCache,
activeReplyBox: state.stream.activeReplyBox,
@@ -224,7 +224,7 @@ const mapStateToProps = state => ({
previousTab: state.embed.previousTab,
});
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showSignInDialog,
addNotification,
@@ -236,12 +236,13 @@ const mapDispatchToProps = dispatch =>
export default compose(
withFragments(fragments),
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
withPostComment,
withPostFlag,
withPostDontAgree,
withAddCommentTag,
withRemoveCommentTag,
withIgnoreUser,
withDeleteAction,
withEditComment,
)(StreamContainer);
@@ -0,0 +1,270 @@
import {gql} from 'react-apollo';
import {add} from 'coral-framework/services/graphqlRegistry';
import update from 'immutability-helper';
const extension = {
fragments: {
EditCommentResponse: gql`
fragment CoralEmbedStream_EditCommentResponse on EditCommentResponse {
comment {
status
}
errors {
translation_key
}
}
`,
StopIgnoringUserResponse: gql`
fragment CoralEmbedStream_StopIgnoringUserResponse on StopIgnoringUserResponse {
errors {
translation_key
}
}
`,
IgnoreUserResponse: gql`
fragment CoralEmbedStream_IgnoreUserResponse on IgnoreUserResponse {
errors {
translation_key
}
}
`,
RemoveCommentTagResponse: gql`
fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
`,
AddCommentTagResponse: gql`
fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
`,
DeleteActionResponse: gql`
fragment CoralEmbedStream_DeleteActionResponse on DeleteActionResponse {
errors {
translation_key
}
}
`,
CreateFlagResponse: gql`
fragment CoralEmbedStream_CreateFlagResponse on CreateFlagResponse {
flag {
id
}
errors {
translation_key
}
}
`,
CreateDontAgreeResponse : gql`
fragment CoralEmbedStream_CreateDontAgreeResponse on CreateDontAgreeResponse {
dontagree {
id
}
errors {
translation_key
}
}
`,
CreateCommentResponse: gql`
fragment CoralEmbedStream_CreateCommentResponse on CreateCommentResponse {
comment {
...CoralEmbedStream_CreateCommentResponse_Comment
replies {
...CoralEmbedStream_CreateCommentResponse_Comment
}
}
errors {
translation_key
}
}
fragment CoralEmbedStream_CreateCommentResponse_Comment on Comment {
id
body
created_at
status
replyCount
tags {
name
}
user {
id
name: username
}
action_summaries {
count
current_user {
id
created_at
}
}
editing {
edited
editableUntil
}
}
`,
},
mutations: {
IgnoreUser: () => ({
// TODO: don't rely on refetching.
refetchQueries: [
'EmbedQuery', 'EmbedStreamProfileQuery',
],
}),
StopIgnoringUser: () => ({
// TODO: don't rely on refetching.
refetchQueries: [
'EmbedQuery', 'EmbedStreamProfileQuery',
],
}),
PostComment: ({
variables: {comment: {asset_id, body, parent_id, tags = []}},
state: {auth},
}) => ({
optimisticResponse: {
createComment: {
comment: {
user: {
id: auth.toJS().user.id,
name: auth.toJS().user.username
},
created_at: new Date().toISOString(),
body,
parent_id,
asset_id,
action_summaries: [],
tags,
status: null,
id: 'pending'
}
}
},
updateQueries: {
EmbedQuery: (previousData, {mutationResult: {data: {createComment: {comment}}}}) => {
if (previousData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return previousData;
}
let updatedAsset;
// If posting a reply
if (parent_id) {
updatedAsset = {
...previousData,
asset: {
...previousData.asset,
comments: previousData.asset.comments.map((oldComment) => {
return oldComment.id === parent_id
? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1}
: oldComment;
})
}
};
} else {
// If posting a top-level comment
updatedAsset = {
...previousData,
asset: {
...previousData.asset,
commentCount: previousData.asset.commentCount + 1,
comments: [comment, ...previousData.asset.comments]
}
};
}
return updatedAsset;
}
}
}),
EditComment: ({
variables: {id, edit},
}) => ({
updateQueries: {
EmbedQuery: (previousData, {mutationResult: {data: {editComment: {comment: {status}}}}}) => {
const updateCommentWithEdit = (comment, edit) => {
const {body} = edit;
const editedComment = update(comment, {
$merge: {
body
},
editing: {$merge:{edited:true}}
});
return editedComment;
};
const commentIsStillVisible = (comment) => {
return !((id === comment.id) && (['PREMOD', 'REJECTED'].includes(status)));
};
const resultReflectingEdit = update(previousData, {
asset: {
comments: {
$apply: (comments) => {
return comments.filter(commentIsStillVisible).map((comment) => {
let replyWasEditedToBeHidden = false;
if (comment.id === id) {
return updateCommentWithEdit(comment, edit);
}
const commentWithUpdatedReplies = update(comment, {
replies: {
$apply: (comments) => {
return comments
.filter((c) => {
if (commentIsStillVisible(c)) {
return true;
}
replyWasEditedToBeHidden = true;
return false;
})
.map((comment) => {
if (comment.id === id) {
return updateCommentWithEdit(comment, edit);
}
return comment;
});
}
},
});
// If a reply was edited to be hdiden, then this parent needs its replyCount to be decremented.
if (replyWasEditedToBeHidden) {
return update(commentWithUpdatedReplies, {
replyCount: {
$apply: (replyCount) => {
return replyCount - 1;
}
}
});
}
return commentWithUpdatedReplies;
});
}
}
}
});
return resultReflectingEdit;
},
},
}),
},
};
add(extension);
+9
View File
@@ -4,10 +4,13 @@ import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import {checkLogin} from 'coral-framework/actions/auth';
import './graphql';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
import reducers from './reducers';
import localStore, {injectReducers} from 'coral-framework/services/store';
import AppRouter from './AppRouter';
import {pym} from 'coral-framework';
injectReducers(reducers);
@@ -16,6 +19,12 @@ const store = (window.opener && window.opener.coralStore) ? window.opener.coralS
// Don't run this in the popup.
if (store === localStore) {
store.dispatch(checkLogin());
pym.sendMessage('getConfig');
pym.onMessage('config', (config) => {
store.dispatch(addExternalConfig(JSON.parse(config)));
});
}
render(
@@ -0,0 +1,15 @@
import {ADD_EXTERNAL_CONFIG} from '../constants/config';
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
case ADD_EXTERNAL_CONFIG:
return {
...state,
...action.config
};
default:
return state;
}
}
@@ -1,7 +1,9 @@
import stream from './stream';
import embed from './embed';
import config from './config';
import stream from './stream';
export default {
stream,
embed,
stream,
config
};
+3 -4
View File
@@ -191,12 +191,13 @@ hr {
}
.coral-plugin-commentbox-textarea {
color: #262626;
flex: 1;
padding: 5px;
padding: 1em;
min-height: 100px;
margin-top: 10px;
font-size: 16px;
border: 1px solid #ccc;
border: 1px solid #9E9E9E;
}
.coral-plugin-commentbox-button-container {
@@ -317,9 +318,7 @@ button.comment__action-button[disabled],
}
.coral-plugin-pubdate-text {
color: #696969;
display: inline-block;
font-size: .75rem;
margin-left: 5px;
}
+43 -40
View File
@@ -22,18 +22,18 @@ const snackbarStyles = {
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box',
fontFamily: 'Helvetica, \'Helvetica Neue\', Verdana, sans-serif'
fontFamily: 'Helvetica, "Helvetica Neue", Verdana, sans-serif'
};
// This function should return value of window.Coral
const Coral = {};
const Talk = Coral.Talk = {};
const Talk = (Coral.Talk = {});
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = [
talkBaseUrl,
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
talkBaseUrl.match(/\/$/) ? '' : '/', // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?'
].join('');
@@ -44,11 +44,21 @@ function buildStreamIframeUrl(talkBaseUrl, query) {
// Set up postMessage listeners/handlers on the pymParent
// e.g. to resize the iframe, and navigate the host page
function configurePymParent(pymParent) {
function configurePymParent(pymParent, opts) {
let notificationOffset = 200;
let ready = false;
let cachedHeight;
const snackbar = document.createElement('div');
// Sends config to pymChild
function sendConfig(config) {
pymParent.sendMessage('config', JSON.stringify(config));
}
// Sends config to the child
pymParent.onMessage('getConfig', function() {
sendConfig(opts || {});
});
snackbar.id = 'coral-notif';
for (let key in snackbarStyles) {
@@ -69,12 +79,12 @@ function configurePymParent(pymParent) {
}
});
pymParent.onMessage('coral-clear-notification', function () {
pymParent.onMessage('coral-clear-notification', function() {
snackbar.style.opacity = 0;
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-all-comments', function () {
pymParent.onMessage('coral-view-all-comments', function() {
window.history.replaceState(
{},
document.title,
@@ -82,7 +92,7 @@ function configurePymParent(pymParent) {
);
});
pymParent.onMessage('coral-alert', function (message) {
pymParent.onMessage('coral-alert', function(message) {
const [type, text] = message.split('|');
snackbar.style.transform = 'translate(-50%, 20px)';
snackbar.style.opacity = 0;
@@ -110,39 +120,21 @@ function configurePymParent(pymParent) {
pymParent.sendMessage('position', position);
});
// Tell child when parent's DOMContentLoaded
pymParent.onMessage('childReady', function () {
const interval = setInterval(function () {
if (ready) {
window.clearInterval(interval);
// TODO: It's weird to me that this is sent here
pymParent.sendMessage('DOMContentLoaded');
}
}, 100);
});
// When end-user clicks link in iframe, open it in parent context
pymParent.onMessage('navigate', function (url) {
pymParent.onMessage('navigate', function(url) {
window.open(url, '_blank').focus();
});
// wait till images and other iframes are loaded before scrolling the page.
// or do we want to be more aggressive and scroll when we hit DOM ready?
document.addEventListener('DOMContentLoaded', function () {
ready = true;
});
// get dimensions of viewport
const viewport = () => {
let e = window, a = 'inner';
if ( !( 'innerWidth' in window ) ){
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
return {
width : e[`${a}Width`],
height : e[`${a}Height`]
width: e[`${a}Width`],
height: e[`${a}Height`]
};
};
}
@@ -156,18 +148,24 @@ function configurePymParent(pymParent) {
* @param {String} [opts.asset_url] - Asset URL
* @param {String} [opts.asset_id] - Asset ID
*/
Talk.render = function (el, opts) {
Talk.render = function(el, opts) {
if (!el) {
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
throw new Error(
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
);
}
if (typeof el !== 'object') {
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
throw new Error(
`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`
);
}
opts = opts || {};
// TODO: infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
if (!opts.talk) {
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
throw new Error(
'Coral.Talk.render() expects opts.talk as the Talk Base URL'
);
}
// Ensure el has an id, as pym can't directly accept the HTMLElement.
@@ -186,16 +184,21 @@ Talk.render = function (el, opts) {
try {
query.asset_url = document.querySelector('link[rel="canonical"]').href;
} catch (e) {
console.warn('This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.');
console.warn(
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
);
query.asset_url = window.location.origin + window.location.pathname;
}
}
configurePymParent(new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), {
title: opts.title,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
}));
configurePymParent(
new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), {
title: opts.title,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
}),
opts
);
};
export default Coral;
+8 -8
View File
@@ -6,14 +6,14 @@ import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
export const fetchAssetRequest = () => ({type: actions.FETCH_ASSET_REQUEST});
export const fetchAssetSuccess = asset => ({type: actions.FETCH_ASSET_SUCCESS, asset});
export const fetchAssetFailure = error => ({type: actions.FETCH_ASSET_FAILURE, error});
export const fetchAssetSuccess = (asset) => ({type: actions.FETCH_ASSET_SUCCESS, asset});
export const fetchAssetFailure = (error) => ({type: actions.FETCH_ASSET_FAILURE, error});
const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_REQUEST});
const updateAssetSettingsSuccess = settings => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsFailure = () => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE});
export const updateConfiguration = newConfig => (dispatch, getState) => {
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
const assetId = getState().asset.toJS().id;
dispatch(updateAssetSettingsRequest());
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
@@ -21,10 +21,10 @@ export const updateConfiguration = newConfig => (dispatch, getState) => {
dispatch(addNotification('success', lang.t('framework.success_update_settings')));
dispatch(updateAssetSettingsSuccess(newConfig));
})
.catch(error => dispatch(updateAssetSettingsFailure(error)));
.catch((error) => dispatch(updateAssetSettingsFailure(error)));
};
export const updateOpenStream = closedBody => (dispatch, getState) => {
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
const assetId = getState().asset.toJS().id;
dispatch(fetchAssetRequest());
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
@@ -32,13 +32,13 @@ export const updateOpenStream = closedBody => (dispatch, getState) => {
dispatch(addNotification('success', lang.t('framework.success_update_settings')));
dispatch(fetchAssetSuccess(closedBody));
})
.catch(error => dispatch(fetchAssetFailure(error)));
.catch((error) => dispatch(fetchAssetFailure(error)));
};
const openStream = () => ({type: actions.OPEN_COMMENTS});
const closeStream = () => ({type: actions.CLOSE_COMMENTS});
export const updateOpenStatus = status => dispatch => {
export const updateOpenStatus = (status) => (dispatch) => {
if (status === 'open') {
dispatch(openStream());
dispatch(updateOpenStream({closedAt: null}));
+168 -103
View File
@@ -1,37 +1,15 @@
import {gql} from 'react-apollo';
import client from 'coral-framework/services/client';
import I18n from '../../coral-i18n/modules/i18n/i18n';
const lang = new I18n();
import {pym} from 'coral-framework';
import * as Storage from '../helpers/storage';
import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
import jwtDecode from 'jwt-decode';
const ME_QUERY = gql`
query Me {
me {
status
comments {
id
body
asset {
id
title
url
}
created_at
}
}
}
`;
function fetchMe() {
return client.query({
fetchPolicy: 'network-only',
query: ME_QUERY});
}
const lang = new I18n(translations);
import translations from './../translations';
import I18n from '../../coral-framework/modules/i18n/i18n';
// Dialog Actions
export const showSignInDialog = () => dispatch => {
export const showSignInDialog = () => (dispatch) => {
const signInPopUp = window.open(
'/embed/stream/login',
'Login',
@@ -49,27 +27,41 @@ export const showSignInDialog = () => dispatch => {
signInPopUp.onunload = () => {
if (loaded) {
dispatch(checkLogin());
fetchMe();
}
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => dispatch => {
export const hideSignInDialog = () => (dispatch) => {
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
window.close();
};
export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST});
export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG});
export const hideCreateUsernameDialog = () => ({type: actions.HIDE_CREATEUSERNAME_DIALOG});
export const createUsernameRequest = () => ({
type: actions.CREATE_USERNAME_REQUEST
});
export const showCreateUsernameDialog = () => ({
type: actions.SHOW_CREATEUSERNAME_DIALOG
});
export const hideCreateUsernameDialog = () => ({
type: actions.HIDE_CREATEUSERNAME_DIALOG
});
const createUsernameSuccess = () => ({type: actions.CREATE_USERNAME_SUCCESS});
const createUsernameFailure = error => ({type: actions.CREATE_USERNAME_FAILURE, error});
const createUsernameSuccess = () => ({
type: actions.CREATE_USERNAME_SUCCESS
});
export const updateUsername = ({username}) => ({type: actions.UPDATE_USERNAME, username});
const createUsernameFailure = (error) => ({
type: actions.CREATE_USERNAME_FAILURE,
error
});
export const createUsername = (userId, formData) => dispatch => {
export const updateUsername = ({username}) => ({
type: actions.UPDATE_USERNAME,
username
});
export const createUsername = (userId, formData) => (dispatch) => {
dispatch(createUsernameRequest());
coralApi('/account/username', {method: 'PUT', body: formData})
.then(() => {
@@ -77,18 +69,18 @@ export const createUsername = (userId, formData) => dispatch => {
dispatch(hideCreateUsernameDialog());
dispatch(updateUsername(formData));
})
.catch(error => {
.catch((error) => {
dispatch(createUsernameFailure(lang.t(`error.${error.translation_key}`)));
});
};
export const changeView = view => dispatch => {
export const changeView = (view) => (dispatch) => {
dispatch({
type: actions.CHANGE_VIEW,
view
});
switch(view) {
switch (view) {
case 'SIGNUP':
window.resizeTo(500, 800);
break;
@@ -100,26 +92,49 @@ export const changeView = view => dispatch => {
}
};
export const cleanState = () => ({type: actions.CLEAN_STATE});
export const cleanState = () => ({
type: actions.CLEAN_STATE
});
// Sign In Actions
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInRequest = () => ({
type: actions.FETCH_SIGNIN_REQUEST
});
// TODO: revisit login redux flow.
// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
//
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
const signInFailure = (error) => ({
type: actions.FETCH_SIGNIN_FAILURE,
error
});
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch) => {
Storage.setItem('exp', jwtDecode(token).exp);
Storage.setItem('token', token);
dispatch({type: 'HANDLE_AUTH_TOKEN'});
};
//==============================================================================
// SIGN IN
//==============================================================================
export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
return coralApi('/auth/local', {method: 'POST', body: formData})
.then(() => dispatch(hideSignInDialog()))
.catch(error => {
.then(({token}) => {
dispatch(handleAuthToken(token));
dispatch(hideSignInDialog());
})
.catch((error) => {
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(signInFailure(lang.t('error.email_not_verified', error.metadata)));
dispatch(
signInFailure(lang.t('error.email_not_verified', error.metadata))
);
} else {
// invalid credentials
@@ -128,13 +143,23 @@ export const fetchSignIn = (formData) => (dispatch) => {
});
};
// Sign In - Facebook
//==============================================================================
// SIGN IN - FACEBOOK
//==============================================================================
const signInFacebookRequest = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST});
const signInFacebookSuccess = user => ({type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS, user});
const signInFacebookFailure = error => ({type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE, error});
const signInFacebookRequest = () => ({
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST
});
const signInFacebookSuccess = (user) => ({
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
user
});
const signInFacebookFailure = (error) => ({
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
error
});
export const fetchSignInFacebook = () => dispatch => {
export const fetchSignInFacebook = () => (dispatch) => {
dispatch(signInFacebookRequest());
window.open(
`${base}/auth/facebook`,
@@ -143,11 +168,15 @@ export const fetchSignInFacebook = () => dispatch => {
);
};
// Sign Up Facebook
//==============================================================================
// SIGN UP - FACEBOOK
//==============================================================================
const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST});
const signUpFacebookRequest = () => ({
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST
});
export const fetchSignUpFacebook = () => dispatch => {
export const fetchSignUpFacebook = () => (dispatch) => {
dispatch(signUpFacebookRequest());
window.open(
`${base}/auth/facebook`,
@@ -156,7 +185,7 @@ export const fetchSignUpFacebook = () => dispatch => {
);
};
export const facebookCallback = (err, data) => dispatch => {
export const facebookCallback = (err, data) => (dispatch) => {
if (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -173,20 +202,26 @@ export const facebookCallback = (err, data) => dispatch => {
}
};
// Sign Up Actions
//==============================================================================
// SIGN UP
//==============================================================================
const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
dispatch(signUpRequest());
coralApi('/users', {method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri}})
coralApi('/users', {
method: 'POST',
body: formData,
headers: {'X-Pym-Url': redirectUri}
})
.then(({user}) => {
dispatch(signUpSuccess(user));
})
.catch(error => {
.catch((error) => {
let errorMessage = lang.t(`error.${error.message}`);
// if there is no translation defined, just show the error string
@@ -197,75 +232,105 @@ export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
});
};
// Forgot Password Actions
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
});
export const fetchForgotPassword = email => (dispatch) => {
dispatch(forgotPassowordRequest(email));
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPasswordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
});
export const fetchForgotPassword = (email) => (dispatch) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = pym.parentUrl || location.href;
coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
coralApi('/account/password/reset', {
method: 'POST',
body: {email, loc: redirectUri}
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => dispatch(forgotPasswordFailure(error)));
};
// LogOut Actions
//==============================================================================
// LOGOUT
//==============================================================================
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
.then(() => {
dispatch(logOutSuccess());
fetchMe();
})
.catch(error => dispatch(logOutFailure(error)));
export const logout = () => (dispatch) => {
return coralApi('/auth', {method: 'DELETE'}).then(() => {
dispatch({type: actions.LOGOUT});
Storage.removeItem('token');
});
};
// LogOut Actions
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
// Check Login
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
const checkLoginFailure = (error) => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
});
export const checkLogin = () => (dispatch) => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then((result) => {
if (!result.user) {
Storage.removeItem('token');
throw new Error('Not logged in');
}
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
const isAdmin = !!result.user.roles.filter((i) => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => {
.catch((error) => {
console.error(error);
dispatch(checkLoginFailure(`${error.translation_key}`));
});
};
const verifyEmailRequest = () => ({type: actions.VERIFY_EMAIL_REQUEST});
const verifyEmailSuccess = () => ({type: actions.VERIFY_EMAIL_SUCCESS});
const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE});
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = (error) => ({type: actions.INVALID_FORM, error});
export const requestConfirmEmail = (email, redirectUri) => dispatch => {
//==============================================================================
// VERIFY EMAIL
//==============================================================================
const verifyEmailRequest = () => ({
type: actions.VERIFY_EMAIL_REQUEST
});
const verifyEmailSuccess = () => ({
type: actions.VERIFY_EMAIL_SUCCESS
});
const verifyEmailFailure = () => ({
type: actions.VERIFY_EMAIL_FAILURE
});
export const requestConfirmEmail = (email, redirectUri) => (dispatch) => {
dispatch(verifyEmailRequest());
return coralApi('/users/resend-verify', {method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri}})
return coralApi('/users/resend-verify', {
method: 'POST',
body: {email},
headers: {'X-Pym-Url': redirectUri}
})
.then(() => {
dispatch(verifyEmailSuccess());
})
.catch(err => {
.catch((err) => {
// email might have already been verifyed
dispatch(verifyEmailFailure(err));
+2 -2
View File
@@ -5,7 +5,7 @@ import * as actions from '../constants/auth';
import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
const editUsernameFailure = error => ({type: actions.EDIT_USERNAME_FAILURE, error});
const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error});
const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS});
export const editName = (username) => (dispatch) => {
@@ -14,7 +14,7 @@ export const editName = (username) => (dispatch) => {
dispatch(editUsernameSuccess());
dispatch(addNotification('success', lang.t('framework.success_name_update')));
})
.catch(error => {
.catch((error) => {
dispatch(editUsernameFailure(lang.t(`error.${error.translation_key}`)));
});
};
@@ -1,3 +1,7 @@
.inline {
display: inline-block;
}
.debug {
background-color: coral;
}
+9 -3
View File
@@ -1,12 +1,13 @@
import React from 'react';
import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
export default function Slot ({fill, inline = false, ...rest}) {
function Slot ({fill, inline = false, plugin_config: config, ...rest}) {
return (
<div className={cn({[styles.inline]: inline})}>
{getSlotElements(fill, rest)}
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug})}>
{getSlotElements(fill, {...rest, config})}
</div>
);
}
@@ -14,3 +15,8 @@ export default function Slot ({fill, inline = false, ...rest}) {
Slot.propTypes = {
fill: React.PropTypes.string
};
const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config});
export default connect(mapStateToProps, null)(Slot);
@@ -8,4 +8,3 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
+1 -5
View File
@@ -33,9 +33,7 @@ export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGOUT = 'LOGOUT';
export const INVALID_FORM = 'INVALID_FORM';
export const VALID_FORM = 'VALID_FORM';
@@ -44,8 +42,6 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
@@ -1,9 +0,0 @@
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
@@ -0,0 +1,2 @@
// fragments defined here are automatically registered.
export default {};
@@ -1,8 +0,0 @@
fragment actionSummaryView on ActionSummary {
__typename
count
current_user {
id
created_at
}
}
@@ -1,18 +0,0 @@
#import "../fragments/actionSummaryView.graphql"
fragment commentView on Comment {
id
body
created_at
status
tags {
name
}
user {
id
name: username
}
action_summaries {
...actionSummaryView
}
}
+171
View File
@@ -0,0 +1,171 @@
import {gql} from 'react-apollo';
import withMutation from '../hocs/withMutation';
export const withPostComment = withMutation(
gql`
mutation PostComment($comment: CreateCommentInput!) {
createComment(comment: $comment) {
...CreateCommentResponse
}
}
`, {
props: ({mutate}) => ({
postComment: (comment) => {
return mutate({
variables: {
comment
},
});
}
}),
});
export const withEditComment = withMutation(
gql`
mutation EditComment($id: ID!, $asset_id: ID!, $edit: EditCommentInput) {
editComment(id:$id, asset_id:$asset_id, edit:$edit) {
...EditCommentResponse
}
}
`, {
props: ({mutate}) => ({
editComment: (id, asset_id, edit) => {
return mutate({
variables: {
id,
asset_id,
edit,
},
});
}
}),
});
export const withPostFlag = withMutation(
gql`
mutation PostFlag($flag: CreateFlagInput!) {
createFlag(flag: $flag) {
...CreateFlagResponse
}
}
`, {
props: ({mutate}) => ({
postFlag: (flag) => {
return mutate({
variables: {
flag
}
});
}}),
});
export const withPostDontAgree = withMutation(
gql`
mutation CreateDontAgree($dontagree: CreateDontAgreeInput!) {
createDontAgree(dontagree: $dontagree) {
...CreateDontAgreeResponse
}
}
`, {
props: ({mutate}) => ({
postDontAgree: (dontagree) => {
return mutate({
variables: {
dontagree
}
});
}}),
});
export const withDeleteAction = withMutation(
gql`
mutation DeleteAction($id: ID!) {
deleteAction(id:$id) {
...DeleteActionResponse
}
}
`, {
props: ({mutate}) => ({
deleteAction: (id) => {
return mutate({
variables: {
id
}
});
}}),
});
export const withAddCommentTag = withMutation(
gql`
mutation AddCommentTag($id: ID!, $tag: String!) {
addCommentTag(id:$id, tag:$tag) {
...AddCommentTagResponse
}
}
`, {
props: ({mutate}) => ({
addCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
export const withRemoveCommentTag = withMutation(
gql`
mutation RemoveCommentTag($id: ID!, $tag: String!) {
removeCommentTag(id:$id, tag:$tag) {
...RemoveCommentTagResponse
}
}
`, {
props: ({mutate}) => ({
removeCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
export const withIgnoreUser = withMutation(
gql`
mutation IgnoreUser($id: ID!) {
ignoreUser(id:$id) {
...IgnoreUserResponse
}
}
`, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
return mutate({
variables: {
id,
},
});
}}),
});
export const withStopIgnoringUser = withMutation(
gql`
mutation StopIgnoringUser($id: ID!) {
stopIgnoringUser(id:$id) {
...StopIgnoringUserResponse
}
}
`, {
props: ({mutate}) => ({
stopIgnoringUser: ({id}) => {
return mutate({
variables: {
id,
},
});
}}),
});
@@ -1,13 +0,0 @@
mutation AddCommentTag ($id: ID!, $tag: String!) {
addCommentTag(id:$id, tag:$tag) {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
}

Some files were not shown because too many files have changed in this diff Show More