mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 15:23:50 +08:00
Merge branch 'master' into i18n-refactor
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Talk [](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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
//==============================================================================
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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,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});
|
||||
|
||||
@@ -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}`));
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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> <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> <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);
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user