mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
This commit is contained in:
+2
-1
@@ -38,6 +38,7 @@ plugins/*
|
|||||||
!plugins/talk-plugin-google-auth
|
!plugins/talk-plugin-google-auth
|
||||||
!plugins/talk-plugin-ignore-user
|
!plugins/talk-plugin-ignore-user
|
||||||
!plugins/talk-plugin-like
|
!plugins/talk-plugin-like
|
||||||
|
!plugins/talk-plugin-local-auth
|
||||||
!plugins/talk-plugin-love
|
!plugins/talk-plugin-love
|
||||||
!plugins/talk-plugin-member-since
|
!plugins/talk-plugin-member-since
|
||||||
!plugins/talk-plugin-mod
|
!plugins/talk-plugin-mod
|
||||||
@@ -53,6 +54,7 @@ plugins/*
|
|||||||
!plugins/talk-plugin-profile-data
|
!plugins/talk-plugin-profile-data
|
||||||
!plugins/talk-plugin-remember-sort
|
!plugins/talk-plugin-remember-sort
|
||||||
!plugins/talk-plugin-respect
|
!plugins/talk-plugin-respect
|
||||||
|
!plugins/talk-plugin-rich-text
|
||||||
!plugins/talk-plugin-slack-notifications
|
!plugins/talk-plugin-slack-notifications
|
||||||
!plugins/talk-plugin-sort-most-downvoted
|
!plugins/talk-plugin-sort-most-downvoted
|
||||||
!plugins/talk-plugin-sort-most-liked
|
!plugins/talk-plugin-sort-most-liked
|
||||||
@@ -66,7 +68,6 @@ plugins/*
|
|||||||
!plugins/talk-plugin-toxic-comments
|
!plugins/talk-plugin-toxic-comments
|
||||||
!plugins/talk-plugin-upvote
|
!plugins/talk-plugin-upvote
|
||||||
!plugins/talk-plugin-viewing-options
|
!plugins/talk-plugin-viewing-options
|
||||||
!plugins/talk-plugin-rich-text
|
|
||||||
|
|
||||||
**/node_modules/*
|
**/node_modules/*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
"exceptions": [
|
"exceptions": [
|
||||||
"https://nodesecurity.io/advisories/531",
|
"https://nodesecurity.io/advisories/531",
|
||||||
"https://nodesecurity.io/advisories/532",
|
"https://nodesecurity.io/advisories/532",
|
||||||
"https://nodesecurity.io/advisories/566"
|
"https://nodesecurity.io/advisories/566",
|
||||||
|
"https://nodesecurity.io/advisories/577",
|
||||||
|
"https://nodesecurity.io/advisories/594",
|
||||||
|
"https://nodesecurity.io/advisories/603",
|
||||||
|
"https://nodesecurity.io/advisories/611",
|
||||||
|
"https://nodesecurity.io/advisories/612"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require('./util');
|
|
||||||
const program = require('commander');
|
const program = require('commander');
|
||||||
const { head, map } = require('lodash');
|
|
||||||
const Matcher = require('did-you-mean');
|
|
||||||
|
|
||||||
|
// We're requiring this here so it'll setup some promise rejection hooks to log
|
||||||
|
// out.
|
||||||
|
require('./util');
|
||||||
|
|
||||||
|
// Setup the program.
|
||||||
program
|
program
|
||||||
.command('serve', 'serve the application')
|
.command('serve', 'serve the application')
|
||||||
.command('db', 'run database commands')
|
.command('db', 'run database commands')
|
||||||
@@ -24,20 +22,3 @@ program
|
|||||||
'provides utilities for interacting with the plugin system'
|
'provides utilities for interacting with the plugin system'
|
||||||
)
|
)
|
||||||
.parse(process.argv);
|
.parse(process.argv);
|
||||||
|
|
||||||
// If the command wasn't found, output help.
|
|
||||||
const commands = map(program.commands, '_name');
|
|
||||||
const command = head(program.args);
|
|
||||||
if (!commands.includes(command)) {
|
|
||||||
const m = new Matcher(commands);
|
|
||||||
const similarCommands = m.list(command);
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
`cli '${command}' is not a talk cli command. See 'cli --help'.`
|
|
||||||
);
|
|
||||||
if (similarCommands.length > 0) {
|
|
||||||
const sc = similarCommands.map(({ value }) => `\t${value}\n`).join('');
|
|
||||||
console.error(`\nThe most similar commands are\n${sc}`);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|||||||
+52
-23
@@ -23,23 +23,33 @@ util.onshutdown([() => mongoose.disconnect()]);
|
|||||||
/**
|
/**
|
||||||
* Lists all the assets registered in the database.
|
* Lists all the assets registered in the database.
|
||||||
*/
|
*/
|
||||||
async function listAssets() {
|
async function listAssets(opts) {
|
||||||
try {
|
try {
|
||||||
let assets = await AssetModel.find({}).sort({ created_at: 1 });
|
let assets = await AssetModel.find({}).sort({ created_at: 1 });
|
||||||
|
|
||||||
let table = new Table({
|
switch (opts.format) {
|
||||||
head: ['ID', 'Title', 'URL'],
|
case 'json': {
|
||||||
});
|
console.log(JSON.stringify(assets, null, 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
let table = new Table({
|
||||||
|
head: ['ID', 'Title', 'URL'],
|
||||||
|
});
|
||||||
|
|
||||||
assets.forEach(asset => {
|
assets.forEach(asset => {
|
||||||
table.push([
|
table.push([
|
||||||
asset.id,
|
asset.id,
|
||||||
asset.title ? asset.title : '',
|
asset.title ? asset.title : '',
|
||||||
asset.url ? asset.url : '',
|
asset.url ? asset.url : '',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(table.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(table.toString());
|
|
||||||
util.shutdown();
|
util.shutdown();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -49,12 +59,13 @@ async function listAssets() {
|
|||||||
|
|
||||||
async function refreshAssets(ageString) {
|
async function refreshAssets(ageString) {
|
||||||
try {
|
try {
|
||||||
const now = new Date().getTime();
|
const query = AssetModel.find({}, { id: 1 });
|
||||||
const ageMs = parseDuration(ageString);
|
if (ageString) {
|
||||||
const age = new Date(now - ageMs);
|
// An age was specified, so filter only those assets.
|
||||||
|
const ageMs = parseDuration(ageString);
|
||||||
|
const age = new Date(Date.now() - ageMs);
|
||||||
|
|
||||||
let assets = await AssetModel.find(
|
query.merge({
|
||||||
{
|
|
||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
scraped: {
|
scraped: {
|
||||||
@@ -65,16 +76,28 @@ async function refreshAssets(ageString) {
|
|||||||
scraped: null,
|
scraped: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
});
|
||||||
{ id: 1 }
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// Create a graph context.
|
// Create a graph context.
|
||||||
const ctx = Context.forSystem();
|
const ctx = Context.forSystem();
|
||||||
|
|
||||||
|
// Load the assets.
|
||||||
|
const cursor = query.cursor();
|
||||||
|
|
||||||
// Queue all the assets for scraping.
|
// Queue all the assets for scraping.
|
||||||
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
|
const promises = [];
|
||||||
console.log('Assets were queued to be scraped');
|
|
||||||
|
let asset = await cursor.next();
|
||||||
|
while (asset) {
|
||||||
|
promises.push(scraper.create(ctx, asset.id));
|
||||||
|
asset = await cursor.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
console.log(`${promises.length} Assets were queued to be scraped.`);
|
||||||
|
|
||||||
util.shutdown();
|
util.shutdown();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -202,11 +225,17 @@ async function rewrite(search, replace, options) {
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command('list')
|
.command('list')
|
||||||
|
.option(
|
||||||
|
'--format <type>',
|
||||||
|
'Specify the output format [table]',
|
||||||
|
/^(table|json)$/i,
|
||||||
|
'table'
|
||||||
|
)
|
||||||
.description('list all the assets in the database')
|
.description('list all the assets in the database')
|
||||||
.action(listAssets);
|
.action(listAssets);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('refresh <age>')
|
.command('refresh [age]')
|
||||||
.description('queues the assets that exceed the age requested')
|
.description('queues the assets that exceed the age requested')
|
||||||
.action(refreshAssets);
|
.action(refreshAssets);
|
||||||
|
|
||||||
|
|||||||
+11
-8
@@ -4,7 +4,8 @@ const util = require('./util');
|
|||||||
const program = require('commander');
|
const program = require('commander');
|
||||||
const inquirer = require('inquirer');
|
const inquirer = require('inquirer');
|
||||||
const mongoose = require('../services/mongoose');
|
const mongoose = require('../services/mongoose');
|
||||||
const SettingsService = require('../services/settings');
|
const Settings = require('../services/settings');
|
||||||
|
const cache = require('../services/cache');
|
||||||
|
|
||||||
// Register the shutdown criteria.
|
// Register the shutdown criteria.
|
||||||
util.onshutdown([() => mongoose.disconnect()]);
|
util.onshutdown([() => mongoose.disconnect()]);
|
||||||
@@ -14,9 +15,12 @@ util.onshutdown([() => mongoose.disconnect()]);
|
|||||||
*/
|
*/
|
||||||
async function changeOrgName() {
|
async function changeOrgName() {
|
||||||
try {
|
try {
|
||||||
let settings = await SettingsService.retrieve();
|
await cache.init();
|
||||||
|
|
||||||
let { organizationName } = await inquirer.prompt([
|
// Get the original settings.
|
||||||
|
const settings = await Settings.select('organizationName');
|
||||||
|
|
||||||
|
const { organizationName } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
name: 'organizationName',
|
name: 'organizationName',
|
||||||
message: 'Organization Name',
|
message: 'Organization Name',
|
||||||
@@ -25,9 +29,8 @@ async function changeOrgName() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (settings.organizationName !== organizationName) {
|
if (settings.organizationName !== organizationName) {
|
||||||
settings.organizationName = organizationName;
|
// Set the organization name if there was a mutation to it.
|
||||||
|
await Settings.update({ organizationName });
|
||||||
await SettingsService.update(settings);
|
|
||||||
|
|
||||||
console.log('Settings were updated.');
|
console.log('Settings were updated.');
|
||||||
} else {
|
} else {
|
||||||
@@ -36,9 +39,9 @@ async function changeOrgName() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
util.shutdown(1);
|
util.shutdown(1);
|
||||||
|
} finally {
|
||||||
|
util.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
util.shutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|||||||
+1
-1
@@ -328,7 +328,7 @@ program
|
|||||||
.action(searchUsers);
|
.action(searchUsers);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('set-role <userID> <role>')
|
.command('set-role <userID>')
|
||||||
.description('sets the role on a user')
|
.description('sets the role on a user')
|
||||||
.action(setUserRole);
|
.action(setUserRole);
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -4,7 +4,8 @@ require('../services/env');
|
|||||||
const debug = require('debug')('talk:util');
|
const debug = require('debug')('talk:util');
|
||||||
const { uniq } = require('lodash');
|
const { uniq } = require('lodash');
|
||||||
|
|
||||||
const util = (module.exports = {});
|
// Setup the utilities.
|
||||||
|
const util = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an array of functions that should be executed in the event that the
|
* Stores an array of functions that should be executed in the event that the
|
||||||
@@ -15,7 +16,7 @@ util.toshutdown = [];
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls all the shutdown functions and then ends the process.
|
* Calls all the shutdown functions and then ends the process.
|
||||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
* @param {Number} [defaultCode=0] default return code upon successful shutdown.
|
||||||
*/
|
*/
|
||||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||||
if (signal) {
|
if (signal) {
|
||||||
@@ -63,3 +64,5 @@ process.on('unhandledRejection', err => {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports = util;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const CommentAnimatedEdit = ({ children, body }) => {
|
|||||||
|
|
||||||
CommentAnimatedEdit.propTypes = {
|
CommentAnimatedEdit.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
body: PropTypes.string,
|
body: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentAnimatedEdit;
|
export default CommentAnimatedEdit;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.tombstone {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 1em;
|
||||||
|
color: #1a212f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './CommentDeletedTombstone.css';
|
||||||
|
import t from 'coral-framework/services/i18n';
|
||||||
|
|
||||||
|
const CommentDeletedTombstone = () => (
|
||||||
|
<div className={styles.tombstone}>{t('framework.comment_is_deleted')}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CommentDeletedTombstone;
|
||||||
@@ -96,8 +96,6 @@ class UserDetail extends React.Component {
|
|||||||
bulkReject,
|
bulkReject,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
console.log(rejectedComments, totalComments);
|
|
||||||
|
|
||||||
// if totalComments is 0, you're dividing by zero
|
// if totalComments is 0, you're dividing by zero
|
||||||
let rejectedPercent = rejectedComments / totalComments * 100;
|
let rejectedPercent = rejectedComments / totalComments * 100;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import CommentAnimatedEdit from './CommentAnimatedEdit';
|
|||||||
import CommentLabels from '../containers/CommentLabels';
|
import CommentLabels from '../containers/CommentLabels';
|
||||||
import ApproveButton from './ApproveButton';
|
import ApproveButton from './ApproveButton';
|
||||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||||
|
import CommentDeletedTombstone from './CommentDeletedTombstone';
|
||||||
|
|
||||||
import t, { timeago } from 'coral-framework/services/i18n';
|
import t, { timeago } from 'coral-framework/services/i18n';
|
||||||
|
|
||||||
@@ -43,6 +44,19 @@ class UserDetailComment extends React.Component {
|
|||||||
body: comment.body,
|
body: comment.body,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!comment.body) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(className, styles.root, {
|
||||||
|
[styles.rootSelected]: selected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CommentDeletedTombstone />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -152,7 +166,7 @@ UserDetailComment.propTypes = {
|
|||||||
comment: PropTypes.shape({
|
comment: PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
body: PropTypes.string.isRequired,
|
body: PropTypes.string,
|
||||||
actions: PropTypes.array,
|
actions: PropTypes.array,
|
||||||
created_at: PropTypes.string.isRequired,
|
created_at: PropTypes.string.isRequired,
|
||||||
asset: PropTypes.shape({
|
asset: PropTypes.shape({
|
||||||
|
|||||||
@@ -291,5 +291,36 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
SetCommentStatus: ({ variables: { status } }) => ({
|
||||||
|
updateQueries: {
|
||||||
|
CoralAdmin_UserDetail: prev => {
|
||||||
|
const increment = {
|
||||||
|
rejectedComments: {
|
||||||
|
$apply: count => (count < prev.totalComments ? count + 1 : count),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrement = {
|
||||||
|
rejectedComments: {
|
||||||
|
$apply: count => (count > 0 ? count - 1 : 0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// If rejected then increment rejectedComments by one
|
||||||
|
if (status === 'REJECTED') {
|
||||||
|
const updated = update(prev, increment);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If approved then decrement rejectedComments by one
|
||||||
|
if (status === 'ACCEPTED') {
|
||||||
|
const updated = update(prev, decrement);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class OrganizationSettings extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updater = { organizationContactEmail: { $set: email } };
|
const updater = { organizationContactEmail: { $set: email } };
|
||||||
const errorUpdater = { organizationEmail: { $set: error } };
|
const errorUpdater = { organizationContactEmail: { $set: error } };
|
||||||
|
|
||||||
this.props.updatePending({ updater, errorUpdater });
|
this.props.updatePending({ updater, errorUpdater });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,6 +85,10 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deleted {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
.moderateArticle {
|
.moderateArticle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
|||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||||
|
import CommentDeletedTombstone from '../../../components/CommentDeletedTombstone';
|
||||||
|
|
||||||
import t, { timeago } from 'coral-framework/services/i18n';
|
import t, { timeago } from 'coral-framework/services/i18n';
|
||||||
|
|
||||||
@@ -75,6 +76,27 @@ class Comment extends React.Component {
|
|||||||
asset: comment.asset,
|
asset: comment.asset,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!comment.body) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(
|
||||||
|
className,
|
||||||
|
'mdl-card',
|
||||||
|
selectionStateCSS,
|
||||||
|
styles.root,
|
||||||
|
{ [styles.selected]: selected, [styles.dangling]: dangling },
|
||||||
|
'talk-admin-moderate-comment',
|
||||||
|
styles.deleted
|
||||||
|
)}
|
||||||
|
id={`comment_${comment.id}`}
|
||||||
|
ref={this.handleRef}
|
||||||
|
>
|
||||||
|
<CommentDeletedTombstone />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -200,7 +222,7 @@ Comment.propTypes = {
|
|||||||
comment: PropTypes.shape({
|
comment: PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
body: PropTypes.string.isRequired,
|
body: PropTypes.string,
|
||||||
action_summaries: PropTypes.array,
|
action_summaries: PropTypes.array,
|
||||||
actions: PropTypes.array,
|
actions: PropTypes.array,
|
||||||
created_at: PropTypes.string.isRequired,
|
created_at: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ const withModQueueQuery = withQuery(
|
|||||||
${Object.keys(queueConfig).map(
|
${Object.keys(queueConfig).map(
|
||||||
queue => `
|
queue => `
|
||||||
${queue}: comments(query: {
|
${queue}: comments(query: {
|
||||||
|
excludeDeleted: true,
|
||||||
statuses: ${
|
statuses: ${
|
||||||
queueConfig[queue].statuses
|
queueConfig[queue].statuses
|
||||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||||
@@ -458,6 +459,7 @@ const withModQueueQuery = withQuery(
|
|||||||
${Object.keys(queueConfig).map(
|
${Object.keys(queueConfig).map(
|
||||||
queue => `
|
queue => `
|
||||||
${queue}Count: commentCount(query: {
|
${queue}Count: commentCount(query: {
|
||||||
|
excludeDeleted: true,
|
||||||
statuses: ${
|
statuses: ${
|
||||||
queueConfig[queue].statuses
|
queueConfig[queue].statuses
|
||||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ ProfileContainer.propTypes = {
|
|||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
const slots = ['profileSections'];
|
const slots = ['profileSections', 'profileSettings', 'profileHeader'];
|
||||||
|
|
||||||
const withProfileQuery = withQuery(
|
const withProfileQuery = withQuery(
|
||||||
gql`
|
gql`
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ AllCommentsPane.propTypes = {
|
|||||||
asset: PropTypes.object,
|
asset: PropTypes.object,
|
||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
postFlag: PropTypes.func,
|
postFlag: PropTypes.func,
|
||||||
postDontAgree: PropTypes.func,
|
postDontAgree: PropTypes.func.isRequired,
|
||||||
loadNewReplies: PropTypes.func,
|
loadNewReplies: PropTypes.func,
|
||||||
deleteAction: PropTypes.func,
|
deleteAction: PropTypes.func,
|
||||||
showSignInDialog: PropTypes.func,
|
showSignInDialog: PropTypes.func,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
getActionSummary,
|
getActionSummary,
|
||||||
iPerformedThisAction,
|
iPerformedThisAction,
|
||||||
isCommentActive,
|
isCommentActive,
|
||||||
|
isCommentDeleted,
|
||||||
getShallowChanges,
|
getShallowChanges,
|
||||||
} from 'coral-framework/utils';
|
} from 'coral-framework/utils';
|
||||||
import t from 'coral-framework/services/i18n';
|
import t from 'coral-framework/services/i18n';
|
||||||
@@ -183,7 +184,7 @@ export default class Comment extends React.Component {
|
|||||||
maxCharCount: PropTypes.number,
|
maxCharCount: PropTypes.number,
|
||||||
root: PropTypes.object,
|
root: PropTypes.object,
|
||||||
loadMore: PropTypes.func,
|
loadMore: PropTypes.func,
|
||||||
postDontAgree: PropTypes.func,
|
postDontAgree: PropTypes.func.isRequired,
|
||||||
animateEnter: PropTypes.bool,
|
animateEnter: PropTypes.bool,
|
||||||
commentClassNames: PropTypes.array,
|
commentClassNames: PropTypes.array,
|
||||||
comment: PropTypes.object.isRequired,
|
comment: PropTypes.object.isRequired,
|
||||||
@@ -409,6 +410,7 @@ export default class Comment extends React.Component {
|
|||||||
charCountEnable,
|
charCountEnable,
|
||||||
showSignInDialog,
|
showSignInDialog,
|
||||||
liveUpdates,
|
liveUpdates,
|
||||||
|
postDontAgree,
|
||||||
emit,
|
emit,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
@@ -439,6 +441,7 @@ export default class Comment extends React.Component {
|
|||||||
key={reply.id}
|
key={reply.id}
|
||||||
comment={reply}
|
comment={reply}
|
||||||
emit={emit}
|
emit={emit}
|
||||||
|
postDontAgree={postDontAgree}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -744,8 +747,14 @@ export default class Comment extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName} id={id}>
|
<div className={rootClassName} id={id}>
|
||||||
{this.renderComment()}
|
{isCommentDeleted(comment) ? (
|
||||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
<CommentTombstone action="deleted" />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{this.renderComment()}
|
||||||
|
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{this.renderRepliesContainer()}
|
{this.renderRepliesContainer()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class CommentTombstone extends React.Component {
|
|||||||
return t('framework.comment_is_ignored');
|
return t('framework.comment_is_ignored');
|
||||||
case 'reject':
|
case 'reject':
|
||||||
return t('framework.comment_is_rejected');
|
return t('framework.comment_is_rejected');
|
||||||
|
case 'deleted':
|
||||||
|
return t('framework.comment_is_deleted');
|
||||||
default:
|
default:
|
||||||
return t('framework.comment_is_hidden');
|
return t('framework.comment_is_hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ StreamContainer.propTypes = {
|
|||||||
commentClassNames: PropTypes.array,
|
commentClassNames: PropTypes.array,
|
||||||
setActiveStreamTab: PropTypes.func,
|
setActiveStreamTab: PropTypes.func,
|
||||||
postFlag: PropTypes.func,
|
postFlag: PropTypes.func,
|
||||||
postDontAgree: PropTypes.func,
|
postDontAgree: PropTypes.func.isRequired,
|
||||||
deleteAction: PropTypes.func,
|
deleteAction: PropTypes.func,
|
||||||
showSignInDialog: PropTypes.func,
|
showSignInDialog: PropTypes.func,
|
||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
@@ -372,6 +372,7 @@ const slots = [
|
|||||||
'streamTabsPrepend',
|
'streamTabsPrepend',
|
||||||
'streamTabPanes',
|
'streamTabPanes',
|
||||||
'streamFilter',
|
'streamFilter',
|
||||||
|
'stream',
|
||||||
];
|
];
|
||||||
|
|
||||||
const fragments = {
|
const fragments = {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export default {
|
|||||||
'UpdateAssetSettingsResponse',
|
'UpdateAssetSettingsResponse',
|
||||||
'UpdateAssetStatusResponse',
|
'UpdateAssetStatusResponse',
|
||||||
'UpdateSettingsResponse',
|
'UpdateSettingsResponse',
|
||||||
'ChangePasswordResponse'
|
'ChangePasswordResponse',
|
||||||
|
'UpdateEmailAddressResponse',
|
||||||
|
'AttachLocalAuthResponse'
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { gql } from 'react-apollo';
|
import { gql } from 'react-apollo';
|
||||||
import withMutation from '../hocs/withMutation';
|
import withMutation from '../hocs/withMutation';
|
||||||
import update from 'immutability-helper';
|
|
||||||
|
|
||||||
function convertItemType(item_type) {
|
function convertItemType(item_type) {
|
||||||
switch (item_type) {
|
switch (item_type) {
|
||||||
@@ -168,36 +167,6 @@ export const withSetCommentStatus = withMutation(
|
|||||||
errors: null,
|
errors: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updateQueries: {
|
|
||||||
CoralAdmin_UserDetail: prev => {
|
|
||||||
const increment = {
|
|
||||||
rejectedComments: {
|
|
||||||
$apply: count =>
|
|
||||||
count < prev.totalComments ? count + 1 : count,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const decrement = {
|
|
||||||
rejectedComments: {
|
|
||||||
$apply: count => (count > 0 ? count - 1 : 0),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// If rejected then increment rejectedComments by one
|
|
||||||
if (status === 'REJECTED') {
|
|
||||||
const updated = update(prev, increment);
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If approved then decrement rejectedComments by one
|
|
||||||
if (status === 'ACCEPTED') {
|
|
||||||
const updated = update(prev, decrement);
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: proxy => {
|
update: proxy => {
|
||||||
const fragment = gql`
|
const fragment = gql`
|
||||||
fragment Talk_SetCommentStatus_Comment on Comment {
|
fragment Talk_SetCommentStatus_Comment on Comment {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { gql } from 'react-apollo';
|
import { gql } from 'react-apollo';
|
||||||
import t from 'coral-framework/services/i18n';
|
import t from 'coral-framework/services/i18n';
|
||||||
import union from 'lodash/union';
|
import union from 'lodash/union';
|
||||||
|
import get from 'lodash/get';
|
||||||
import { capitalize } from 'coral-framework/helpers/strings';
|
import { capitalize } from 'coral-framework/helpers/strings';
|
||||||
import assignWith from 'lodash/assignWith';
|
import assignWith from 'lodash/assignWith';
|
||||||
import mapValues from 'lodash/mapValues';
|
import mapValues from 'lodash/mapValues';
|
||||||
@@ -221,6 +222,13 @@ export function isCommentActive(commentStatus) {
|
|||||||
return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0;
|
return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCommentDeleted(comment) {
|
||||||
|
return (
|
||||||
|
get(comment, 'body', null) === null ||
|
||||||
|
get(comment, 'deleted_at', null) !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getShallowChanges(a, b) {
|
export function getShallowChanges(a, b) {
|
||||||
return union(Object.keys(a), Object.keys(b)).filter(key => a[key] !== b[key]);
|
return union(Object.keys(a), Object.keys(b)).filter(key => a[key] !== b[key]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,13 @@ const CONFIG = {
|
|||||||
RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC,
|
RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC,
|
||||||
RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET,
|
RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET,
|
||||||
|
|
||||||
|
// RECAPTCHA_WINDOW is the rate limit's time interval
|
||||||
|
RECAPTCHA_WINDOW: process.env.TALK_RECAPTCHA_WINDOW || '10m',
|
||||||
|
|
||||||
|
// After RECAPTCHA_INCORRECT_TRIGGER incorrect attempts, recaptcha will be required.
|
||||||
|
RECAPTCHA_INCORRECT_TRIGGER:
|
||||||
|
process.env.TALK_RECAPTCHA_INCORRECT_TRIGGER || 5,
|
||||||
|
|
||||||
// WEBSOCKET_LIVE_URI is the absolute url to the live endpoint.
|
// WEBSOCKET_LIVE_URI is the absolute url to the live endpoint.
|
||||||
WEBSOCKET_LIVE_URI: process.env.TALK_WEBSOCKET_LIVE_URI || null,
|
WEBSOCKET_LIVE_URI: process.env.TALK_WEBSOCKET_LIVE_URI || null,
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -41,7 +41,7 @@ relative_link: false
|
|||||||
future: true
|
future: true
|
||||||
highlight:
|
highlight:
|
||||||
enable: false
|
enable: false
|
||||||
|
|
||||||
# Home page setting
|
# Home page setting
|
||||||
# path: Root path for your blogs index page. (default = '')
|
# path: Root path for your blogs index page. (default = '')
|
||||||
# per_page: Posts displayed per page. (0 = disable pagination)
|
# per_page: Posts displayed per page. (0 = disable pagination)
|
||||||
@@ -114,6 +114,8 @@ sidebar:
|
|||||||
url: /integrating/styling-css/
|
url: /integrating/styling-css/
|
||||||
- title: Translations and i18n
|
- title: Translations and i18n
|
||||||
url: /integrating/translations-i18n/
|
url: /integrating/translations-i18n/
|
||||||
|
- title: GDPR Compliance
|
||||||
|
url: /integrating/gdpr/
|
||||||
- title: Product Guide
|
- title: Product Guide
|
||||||
children:
|
children:
|
||||||
- title: How Talk Works
|
- title: How Talk Works
|
||||||
|
|||||||
@@ -316,6 +316,18 @@ default to providing only a time based lockout. Refer to
|
|||||||
[reCAPTCHA](https://www.google.com/recaptcha/intro/index.html) for information
|
[reCAPTCHA](https://www.google.com/recaptcha/intro/index.html) for information
|
||||||
on getting an account setup.
|
on getting an account setup.
|
||||||
|
|
||||||
|
## TALK_RECAPTCHA_WINDOW
|
||||||
|
|
||||||
|
The rate limit time interval that there can be [TALK_RECAPTCHA_INCORRECT_TRIGGER](#talk_recaptcha_incorrect_trigger) incorrect attempts until the reCAPTCHA is
|
||||||
|
marked as required, parsed by
|
||||||
|
[ms](https://www.npmjs.com/package/ms). (Default `10m`)
|
||||||
|
|
||||||
|
## TALK_RECAPTCHA_INCORRECT_TRIGGER
|
||||||
|
|
||||||
|
The number of times that an incorrect login can be entered before within a time
|
||||||
|
perioud indicated by [TALK_RECAPTCHA_WINDOW](#talk_recaptcha_window) until the
|
||||||
|
reCAPTCHA is marked as required. (Default `5`)
|
||||||
|
|
||||||
## TALK_REDIS_CLIENT_CONFIGURATION
|
## TALK_REDIS_CLIENT_CONFIGURATION
|
||||||
|
|
||||||
Configuration overrides for the redis client configuration in a JSON encoded
|
Configuration overrides for the redis client configuration in a JSON encoded
|
||||||
@@ -531,4 +543,4 @@ Sets the logging level for the context logger (from [Bunyan](https://github.com/
|
|||||||
A JSON string representing the configuration passed to the
|
A JSON string representing the configuration passed to the
|
||||||
[fetch](https://www.npmjs.com/package/node-fetch) call for the scraper. It
|
[fetch](https://www.npmjs.com/package/node-fetch) call for the scraper. It
|
||||||
can be used to set an authorization header, or change the user agent. (Default
|
can be used to set an authorization header, or change the user agent. (Default
|
||||||
`{}`)
|
`{}`)
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ Plugins are additional functionality which are optional to use with Talk. You
|
|||||||
can turn these on or off, depending on your specific needs. Plugins are either
|
can turn these on or off, depending on your specific needs. Plugins are either
|
||||||
part of our core plugins, which ship with Talk, or they are developed by 3rd
|
part of our core plugins, which ship with Talk, or they are developed by 3rd
|
||||||
parties and either used privately and internally, or are open sourced for use
|
parties and either used privately and internally, or are open sourced for use
|
||||||
across the greater community. You can explore the plugins we offer by visiting our [Default Plugins](/talk/default-plugins/)
|
across the greater community. You can explore the plugins we offer by visiting our [Plugin Directory](/talk/plugins-directory/).
|
||||||
and [Additional Plugins](/talk/additional-plugins/) pages.
|
|
||||||
|
|
||||||
## Recipes
|
## Recipes
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ trying to improve a broken part of the internet.
|
|||||||
|
|
||||||
## How do I add the Toxic Comments plugin?
|
## How do I add the Toxic Comments plugin?
|
||||||
To enable this behavior, visit the
|
To enable this behavior, visit the
|
||||||
[talk-plugin-toxic-comments](/talk/additional-plugins/#talk-plugin-toxic-comments)
|
[talk-plugin-toxic-comments](/talk/plugin/talk-plugin-toxic-comments)
|
||||||
plugin documentation.
|
plugin documentation.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,47 +3,53 @@ title: Trust
|
|||||||
permalink: /trust/
|
permalink: /trust/
|
||||||
---
|
---
|
||||||
|
|
||||||
Trust is a set of components within Talk that incorporate automated moderation
|
Trust is a set of components within Talk that incorporate basic automated moderation features based on a user's previous behavior.
|
||||||
features based on a user's previous behavior.
|
|
||||||
|
|
||||||
## User Karma Score
|
## User Karma Score
|
||||||
|
|
||||||
Using Trust’s calculations, Talk will automatically pre-moderate comments of
|
Using Trust’s calculations, Talk will automatically hold back, move to the Reported queue, and tag with a 'History' marker, any comments by users who have an Unreliable karma score. (This is for sites who practice post-moderation. If you set pre-moderation of all comments sitewide, this feature has limited use.)
|
||||||
users who have a negative karma score. All users start out with a `0` neutral
|
|
||||||
karma score. If they have a comment approved by a moderator, their score
|
|
||||||
increases by `1`; if they have a comment rejected by a moderator, it decreases
|
|
||||||
by `1`. When a commenter is labeled as Unreliable, their comments must be
|
|
||||||
moderated before they are posted.
|
|
||||||
|
|
||||||
When a commenter has one comment rejected, their next comment must be moderated
|
All users start out with a Neutral karma score (`0`). If they have a comment approved by a moderator, their score increases by `1`; if they have a comment rejected by a moderator, it decreases by `1`. When a commenter's score is labeled as Unreliable, their comments must be approved from the Reported queue before they are posted. Commenters are shown a message stating that a moderator will review their comment shortly.
|
||||||
once in order to post freely again. If they instead get rejected again, then
|
|
||||||
they must have two of their comments approved in order to get added back to the
|
|
||||||
queue.
|
|
||||||
|
|
||||||
Here are the default thresholds:
|
Here are the default thresholds:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
-2 and lower: Unreliable
|
-1 and lower: Unreliable
|
||||||
-1 to +2: Neutral
|
0 to +1: Neutral
|
||||||
+3 and higher: Reliable
|
+2 and higher: Reliable (we don't do anything with this label right now)
|
||||||
```
|
```
|
||||||
|
|
||||||
You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your
|
So in this case, when a new commenter has their first comment rejected, their user karma score becomes `-1`, which triggers the Unreliable threshhold, and they must then have a comment approved by a moderator in order to post freely again. Until that occurs, all of their comments will be held back temporarily in the Reported queue, marked with a `History` tag.
|
||||||
configuration.
|
|
||||||
|
If their next comment is also rejected, their user karma score is now `-2`, and they must have two comments approved in order to reach a Neutral score, and post without pre-approval.
|
||||||
|
|
||||||
|
We strongly recommend not telling your community how this system works, or where the threshholds lie. Firstly, they might try to game the system to meet approval, and secondly, it makes it harder for you to change the threshhold in the future. We suggest using language such as "We hold back comments for approval for a variety of reasons, including content, account history, and more."
|
||||||
|
|
||||||
|
If you see that a high proportion of first-time commenters on your site are abusive, you might want to change the threshhold to `0`, at least temporarily. You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your site configuration.
|
||||||
|
|
||||||
|
|
||||||
## Reliable and Unreliable Flaggers
|
## Reliable and Unreliable Flaggers
|
||||||
|
|
||||||
Trust also calculates how reliable users are in terms of the comments they
|
Trust also calculates how reliable users are in terms of the comments they
|
||||||
report. This information is displayed to moderators in the User History drawer,
|
report. This information is displayed to moderators in the User History drawer,
|
||||||
which is accessed by clicking on a user’s name in the Admin.
|
which is accessed by clicking on a user’s name in the Admin. Currently, no other action is taken based on this score.
|
||||||
|
|
||||||
If a user's reports mostly match what moderators reject, their Report status
|
If a user's reports mostly match what moderators reject, their Report status
|
||||||
will display to moderators as Reliable in the user information drawer. If a
|
will display to moderators as Reliable in the user information drawer. If a
|
||||||
user's reports mostly differ from what moderators reject, their Report status
|
user's reports mostly differ from what moderators reject, their Report status
|
||||||
will show as Unreliable.
|
will show as Unreliable.
|
||||||
|
|
||||||
If we don't have enough reports to make a call, or the reports even out, their
|
If Talk doesn't have enough reports to make a call, or the reports even out, their
|
||||||
status is Neutral.
|
status is Neutral.
|
||||||
|
|
||||||
|
Here are the default thresholds:
|
||||||
|
|
||||||
|
```text
|
||||||
|
-1 and lower: Unreliable
|
||||||
|
0 to +1: Neutral
|
||||||
|
+2 and higher: Reliable
|
||||||
|
```
|
||||||
|
You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your
|
||||||
|
configuration.
|
||||||
|
|
||||||
Note: Report Karma doesn't include reports of "I don't agree with this comment".
|
Note: Report Karma doesn't include reports of "I don't agree with this comment".
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: GDPR Compliance
|
||||||
|
permalink: /integrating/gdpr/
|
||||||
|
---
|
||||||
|
|
||||||
|
In order to facilitate compliance with the
|
||||||
|
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
|
||||||
|
can enable the following plugins:
|
||||||
|
|
||||||
|
- [talk-plugin-auth](/talk/plugin/talk-plugin-auth) - to facilitate username and password changes
|
||||||
|
- [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association
|
||||||
|
- [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion
|
||||||
|
|
||||||
|
Even if you don't reside in a location where GDPR will apply, it is recommended
|
||||||
|
to enable these features as a best practice to provide your users with control over their
|
||||||
|
own data.
|
||||||
|
|
||||||
|
## GPDR Feature Overview
|
||||||
|
|
||||||
|
Integrating our GDPR tools will give your users and organizations the following benefits:
|
||||||
|
|
||||||
|
- **Download my comment data**: Users can request a download of their comments. An email with a link is emailed to them to download a CSV with each comment they've made, what story it was made on, and the comment's ID and timestamp.
|
||||||
|
- **Delete my account**: Users can request deletion of their account. Deleted account requests are pending for 24 hours to allow the user to download their comments, or to change their mind and reactivate their account before the expiry. Account deletions remove all of their comments from the site, all their comments and actions from the database, and their account info from our system.
|
||||||
|
- **Add an email to an Oauth/external account**: Users are prompted to add an email to their non-Talk account (Facebook, Google, external, etc) so that they can take part in GDPR and other features requiring email communication.
|
||||||
|
- **Change my username**: Users can update their username. This is capped at once every 2 weeks.
|
||||||
|
- **Change my email**: Users can change their email.
|
||||||
|
- **Change my password**: Users can change their password.
|
||||||
|
|
||||||
|
## Custom Authentication Solutions
|
||||||
|
|
||||||
|
As many of the newsrooms who have integrated Talk have followed our guides on
|
||||||
|
[Integrating Authentication](/talk/integrating/authentication/), we have also
|
||||||
|
provided tools for those newsrooms to integrate GDPR features into their
|
||||||
|
existing workflows.
|
||||||
|
|
||||||
|
### Account Data
|
||||||
|
|
||||||
|
Through the [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data)
|
||||||
|
plugin we allow users to download and delete their account data easily. For
|
||||||
|
custom integrations, this isn't always possible, so we instead provide some
|
||||||
|
GraphQL mutations designed to allow you to integrate it into your existing user
|
||||||
|
interfaces or exports.
|
||||||
|
|
||||||
|
- `downloadUser(id: ID!)` - lets you grab the direct link to download a users
|
||||||
|
account in a zip format. From there, you can integrate it into your existing
|
||||||
|
data export or simply proxy it to the user to allow them to download it
|
||||||
|
elsewhere in your UI.
|
||||||
|
- `delUser(id: ID!)` - lets you delete the specified user
|
||||||
|
|
||||||
|
**Note: These mutations require an administrative token**
|
||||||
|
|
||||||
|
If you would prefer to write your own user interfaces or integrate it into your
|
||||||
|
own, you can disable the client plugin for [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data)
|
||||||
|
but keep the server side plugin active (See [Server and Client Plugins](/talk/plugins/#server-and-client-plugins) for more information).
|
||||||
@@ -70,7 +70,7 @@ The most important predictors of the success of an online community are:
|
|||||||
|
|
||||||
* Utilize [our Toxic Comments plugin](https://blog.coralproject.net/toxic-avenging/), developed with Google Jigsaw, to improve commenter behavior and use AI to help identify and prevent the most abusive comments from appearing on your site.
|
* Utilize [our Toxic Comments plugin](https://blog.coralproject.net/toxic-avenging/), developed with Google Jigsaw, to improve commenter behavior and use AI to help identify and prevent the most abusive comments from appearing on your site.
|
||||||
|
|
||||||
* Enable our [Akismet plugin](https://coralproject.github.io/talk/additional-plugins/#talk-plugin-akismet) to keep spam from appearing in your comments.
|
* Enable our [Akismet plugin](/talk/plugin/talk-plugin-akismet) to keep spam from appearing in your comments.
|
||||||
|
|
||||||
* Use keyboard shortcuts in the moderation queue to moderate quickly (type '?' in the moderation view to see the list of shortcuts), and if there is a sudden deluge of comments, ask for someone in your newsroom to help you moderate. Talk will notify you in the moderation interface if someone else moderates a comment that's already on your screen.
|
* Use keyboard shortcuts in the moderation queue to moderate quickly (type '?' in the moderation view to see the list of shortcuts), and if there is a sudden deluge of comments, ask for someone in your newsroom to help you moderate. Talk will notify you in the moderation interface if someone else moderates a comment that's already on your screen.
|
||||||
* Click on a community member's name in the moderation interface to review all their comments, and see if there is a clear pattern of abuse among their Rejected comments. You can also select all their recent comments and delete them in bulk.
|
* Click on a community member's name in the moderation interface to review all their comments, and see if there is a clear pattern of abuse among their Rejected comments. You can also select all their recent comments and delete them in bulk.
|
||||||
@@ -83,7 +83,7 @@ The most important predictors of the success of an online community are:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
* If you find new commenters are causing a lot of trouble with their first comments, consider changing [the User Karma threshold](https://coralproject.github.io/talk/trust/) from negative one to zero, forcing every new commenter's first comment into pre-moderation.
|
* If you find new commenters are causing a lot of trouble with their first comments, consider changing [the User Karma threshold](/talk/trust/) from negative one to zero, forcing every new commenter's first comment into pre-moderation.
|
||||||
|
|
||||||
#### Effectively
|
#### Effectively
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
tags:
|
tags:
|
||||||
- moderation
|
- moderation
|
||||||
- name: talk-plugin-auth
|
- name: talk-plugin-auth
|
||||||
description: Enables internal authentication from Coral.
|
description: Enables internal sign-in authentication.
|
||||||
tags:
|
tags:
|
||||||
- default
|
- default
|
||||||
- auth
|
- auth
|
||||||
|
- gdpr
|
||||||
|
- name: talk-plugin-local-auth
|
||||||
|
description: Enables email and password based authentication.
|
||||||
|
tags:
|
||||||
|
- default
|
||||||
|
- auth
|
||||||
|
- gdpr
|
||||||
- name: talk-plugin-author-menu
|
- name: talk-plugin-author-menu
|
||||||
description: Enables the comment author name plugin area.
|
description: Enables the comment author name plugin area.
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ permalink: /contact/
|
|||||||
|
|
||||||
## How can I get help integrating Talk into my newsroom?
|
## How can I get help integrating Talk into my newsroom?
|
||||||
|
|
||||||
We're here to help with newsrooms of all sizes. Email our Integration Engineer
|
We're here to help with newsrooms of all sizes. Email our Support Team
|
||||||
([jeff@mozillafoundation.org](mailto:jeff@mozillafoundation.org)) to set up a meeting.
|
([support@coralproject.net](mailto:support@coralproject.net)) to set up a meeting.
|
||||||
|
|
||||||
## How do I request a feature or submit a bug?
|
## How do I request a feature or submit a bug?
|
||||||
|
|
||||||
@@ -14,4 +14,4 @@ The best way is to [submit a GitHub issue](https://github.com/coralproject/talk/
|
|||||||
|
|
||||||
## How can our dev team contribute to Talk?
|
## How can our dev team contribute to Talk?
|
||||||
|
|
||||||
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via GitHub, or get in touch directly with Jeff via jeff@mozillafoundation.org.
|
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via GitHub, or get in touch with us directly via support@coralproject.net.
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ example issuer and Talk must match:
|
|||||||
|------|----------------------|
|
|------|----------------------|
|
||||||
|`JWT_ISSUER`|`JWT_ISSUER`|
|
|`JWT_ISSUER`|`JWT_ISSUER`|
|
||||||
|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
|
|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
|
||||||
|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
|
|
||||||
|`SECRET`|`JWT_SECRET`*|
|
|`SECRET`|`JWT_SECRET`*|
|
||||||
|
|
||||||
\* Note that secrets is a pretty complex topic, refer to the
|
\* Note that secrets is a pretty complex topic, refer to the
|
||||||
@@ -83,4 +82,4 @@ reference, the basic takeaway is that the secret used to sign the tokens issued
|
|||||||
by the issuer must be able to be verified by Talk.
|
by the issuer must be able to be verified by Talk.
|
||||||
|
|
||||||
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound)
|
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound)
|
||||||
reference.
|
reference.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../plugins/talk-plugin-local-auth/README.md
|
||||||
@@ -9,11 +9,16 @@ functionality. We provide methods to inject behavior into the server side and
|
|||||||
the client side application to affect different parts of the application
|
the client side application to affect different parts of the application
|
||||||
life cycle.
|
life cycle.
|
||||||
|
|
||||||
## Recipes
|
## Server and Client Plugins
|
||||||
|
|
||||||
Recipes are plugin templates provided by the Coral Core team. Developers can use
|
When you're adding a plugin to Talk, you can specify it in the `client` and/or
|
||||||
these recipes to build their own plugins. You can find all the Talk recipes
|
the `server` section. If you only want to enable the server side component of a
|
||||||
here: [github.com/coralproject/talk-recipes](https://github.com/coralproject/talk-recipes/).
|
plugin, you simply only specify the plugin in the `server` section. If you only
|
||||||
|
want the client side plugin, the `client` section.
|
||||||
|
|
||||||
|
Plugins listed in the [Plugins Directory](/talk/plugins-directory/) will
|
||||||
|
indicate if they have/support a client/server plugin, and should be activated
|
||||||
|
accordingly.
|
||||||
|
|
||||||
## Plugin Registration
|
## Plugin Registration
|
||||||
|
|
||||||
@@ -117,3 +122,9 @@ assets inside the image as well.
|
|||||||
|
|
||||||
For more information on the onbuild image, refer to the
|
For more information on the onbuild image, refer to the
|
||||||
[Installation from Docker](/talk/installation-from-docker/) documentation.
|
[Installation from Docker](/talk/installation-from-docker/) documentation.
|
||||||
|
|
||||||
|
## Recipes
|
||||||
|
|
||||||
|
Recipes are plugin templates provided by the Coral Core team. Developers can use
|
||||||
|
these recipes to build their own plugins. You can find all the Talk recipes
|
||||||
|
here: [github.com/coralproject/talk-recipes](https://github.com/coralproject/talk-recipes/).
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ title: Plugins Directory
|
|||||||
permalink: /plugins-directory/
|
permalink: /plugins-directory/
|
||||||
layout: plugins
|
layout: plugins
|
||||||
data: plugins
|
data: plugins
|
||||||
|
class: plugins
|
||||||
---
|
---
|
||||||
|
|
||||||
Talk provides a growing ecosystem of plugins that interact with our extensive
|
Talk provides a growing ecosystem of plugins that interact with our extensive
|
||||||
Server and Client API's. Below you can search for a plugin to use with Talk and
|
Server and Client API's. Below you can search for a plugin to use with Talk and
|
||||||
discover what their requirements and configuration are.
|
discover what their requirements and configuration are.
|
||||||
|
|||||||
+5
-4
@@ -4,9 +4,9 @@
|
|||||||
<h1>{{ page.title }}</h1>
|
<h1>{{ page.title }}</h1>
|
||||||
<hr/>
|
<hr/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ page.content }}
|
{{ page.content }}
|
||||||
|
|
||||||
<div class="form-group mt-5">
|
<div class="form-group mt-5">
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<label for="exampleInputEmail1">Plugin Search</label>
|
<label for="exampleInputEmail1">Plugin Search</label>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<input type="text" class="form-control" id="plugin-search-input" aria-describedby="pluginSearchHelp">
|
<input type="text" class="form-control" id="plugin-search-input" aria-describedby="pluginSearchHelp">
|
||||||
<small id="pluginSearchHelp" class="form-text text-muted">Enter a few keywords about the plugin to filter the list</small>
|
<small id="pluginSearchHelp" class="form-text text-muted">Enter a few keywords about the plugin to filter the list</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row plugins">
|
<div class="row plugins">
|
||||||
{% for plugin in _.sortBy(site.data[page.data], 'name') %}
|
{% for plugin in _.sortBy(site.data[page.data], 'name') %}
|
||||||
<div class="col-sm-6 plugin d-block">
|
<div class="col-sm-6 plugin d-block">
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
{% if plugin.tags %}
|
{% if plugin.tags %}
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
{% for tag in plugin.tags %}
|
{% for tag in plugin.tags %}
|
||||||
<span class="badge badge-{% if tag == "default" %}success{% else %}light{% endif %}">{{ tag }}</span>
|
<a class="plain-link" title="Search for plugins with the {{ tag }} tag" href="?q={{ tag }}"><span class="badge badge-light badge-tag badge-tag-{{ _.kebabCase(tag) }}">{{ tag }}</span></a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -35,4 +36,4 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
<script>window.SEARCH_INDEX = {{ lunr_index(site.data[page.data]) }}</script>
|
<script>window.SEARCH_INDEX = {{ lunr_index(site.data[page.data]) }}</script>
|
||||||
|
|||||||
+14
@@ -448,3 +448,17 @@ a.brand {
|
|||||||
.plugin {
|
.plugin {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-tag {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-tag-default {
|
||||||
|
background: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-tag-gdpr {
|
||||||
|
background: rgb(0, 102, 176);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|||||||
+17
@@ -1,6 +1,17 @@
|
|||||||
/* global lunr */
|
/* global lunr */
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
// Sourced from https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||||
|
function getParameterByName(name, url) {
|
||||||
|
if (!url) url = window.location.href;
|
||||||
|
name = name.replace(/[\[\]]/g, '\\$&');
|
||||||
|
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||||
|
results = regex.exec(url);
|
||||||
|
if (!results) return null;
|
||||||
|
if (!results[2]) return '';
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||||
|
}
|
||||||
|
|
||||||
// Sourced from https://github.com/hexojs/site/blob/8e8ed4901769abbf76263125f82832df76ced58b/themes/navy/source/js/plugins.js.
|
// Sourced from https://github.com/hexojs/site/blob/8e8ed4901769abbf76263125f82832df76ced58b/themes/navy/source/js/plugins.js.
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -60,6 +71,12 @@
|
|||||||
updateCount(elements.length);
|
updateCount(elements.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var searchParam = getParameterByName('q');
|
||||||
|
if (searchParam && searchParam.length > 0) {
|
||||||
|
$input.value = searchParam;
|
||||||
|
search(searchParam);
|
||||||
|
}
|
||||||
|
|
||||||
$input.addEventListener('input', function() {
|
$input.addEventListener('input', function() {
|
||||||
var value = this.value;
|
var value = this.value;
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,14 @@ class ErrParentDoesNotVisible extends TalkError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ErrPasswordIncorrect extends TalkError {
|
||||||
|
constructor() {
|
||||||
|
super('Your current password was entered incorrectly', {
|
||||||
|
translation_key: 'PASSWORD_INCORRECT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
TalkError,
|
TalkError,
|
||||||
ErrAlreadyExists,
|
ErrAlreadyExists,
|
||||||
@@ -395,6 +403,7 @@ module.exports = {
|
|||||||
ErrNotFound,
|
ErrNotFound,
|
||||||
ErrNotVerified,
|
ErrNotVerified,
|
||||||
ErrParentDoesNotVisible,
|
ErrParentDoesNotVisible,
|
||||||
|
ErrPasswordIncorrect,
|
||||||
ErrPasswordResetToken,
|
ErrPasswordResetToken,
|
||||||
ErrPasswordTooShort,
|
ErrPasswordTooShort,
|
||||||
ErrPermissionUpdateUsername,
|
ErrPermissionUpdateUsername,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const findOrCreateAssetByURL = async (ctx, url) => {
|
|||||||
// Check for whitelisting + get the settings at the same time.
|
// Check for whitelisting + get the settings at the same time.
|
||||||
const [whitelisted, settings] = await Promise.all([
|
const [whitelisted, settings] = await Promise.all([
|
||||||
DomainList.urlCheck(url),
|
DomainList.urlCheck(url),
|
||||||
Settings.load('autoCloseStream closedTimeout'),
|
Settings.select('autoCloseStream', 'closedTimeout'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If the domain wasn't whitelisted, then we shouldn't create this asset!
|
// If the domain wasn't whitelisted, then we shouldn't create this asset!
|
||||||
|
|||||||
+24
-10
@@ -94,6 +94,7 @@ const getCommentCountByQuery = (ctx, options) => {
|
|||||||
author_id,
|
author_id,
|
||||||
tags,
|
tags,
|
||||||
action_type,
|
action_type,
|
||||||
|
excludeDeleted,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||||
@@ -120,6 +121,12 @@ const getCommentCountByQuery = (ctx, options) => {
|
|||||||
query.merge({ author_id });
|
query.merge({ author_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeDeleted) {
|
||||||
|
// The null query matches documents that either contain the `deleted_at`
|
||||||
|
// field whose value is null or that do not contain the `deleted_at` field.
|
||||||
|
query.merge({ deleted_at: null });
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||||
query.merge({
|
query.merge({
|
||||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||||
@@ -328,11 +335,12 @@ const getCommentsByQuery = async (
|
|||||||
sortOrder,
|
sortOrder,
|
||||||
sortBy,
|
sortBy,
|
||||||
excludeIgnored,
|
excludeIgnored,
|
||||||
|
excludeDeleted,
|
||||||
tags,
|
tags,
|
||||||
action_type,
|
action_type,
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
let comments = CommentModel.find();
|
const query = CommentModel.find();
|
||||||
|
|
||||||
// Enforce that the limit must be gte 0 if this option is not true.
|
// Enforce that the limit must be gte 0 if this option is not true.
|
||||||
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
|
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
|
||||||
@@ -350,11 +358,17 @@ const getCommentsByQuery = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (statuses) {
|
if (statuses) {
|
||||||
comments = comments.where({ status: { $in: statuses } });
|
query.merge({ status: { $in: statuses } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDeleted) {
|
||||||
|
// The null query matches documents that either contain the `deleted_at`
|
||||||
|
// field whose value is null or that do not contain the `deleted_at` field.
|
||||||
|
query.merge({ deleted_at: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||||
comments = comments.where({
|
query.merge({
|
||||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||||
$gt: 0,
|
$gt: 0,
|
||||||
},
|
},
|
||||||
@@ -362,7 +376,7 @@ const getCommentsByQuery = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ids) {
|
if (ids) {
|
||||||
comments = comments.find({
|
query.merge({
|
||||||
id: {
|
id: {
|
||||||
$in: ids,
|
$in: ids,
|
||||||
},
|
},
|
||||||
@@ -370,7 +384,7 @@ const getCommentsByQuery = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tags) {
|
if (tags) {
|
||||||
comments = comments.find({
|
query.merge({
|
||||||
'tags.tag.name': {
|
'tags.tag.name': {
|
||||||
$in: tags,
|
$in: tags,
|
||||||
},
|
},
|
||||||
@@ -383,17 +397,17 @@ const getCommentsByQuery = async (
|
|||||||
(ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) &&
|
(ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) &&
|
||||||
author_id != null
|
author_id != null
|
||||||
) {
|
) {
|
||||||
comments = comments.where({ author_id });
|
query.merge({ author_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset_id) {
|
if (asset_id) {
|
||||||
comments = comments.where({ asset_id });
|
query.merge({ asset_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// We perform the undefined check because, null, is a valid state for the
|
// We perform the undefined check because, null, is a valid state for the
|
||||||
// search to be with, which indicates that it is at depth 0.
|
// search to be with, which indicates that it is at depth 0.
|
||||||
if (parent_id !== undefined) {
|
if (parent_id !== undefined) {
|
||||||
comments = comments.where({ parent_id });
|
query.merge({ parent_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -402,12 +416,12 @@ const getCommentsByQuery = async (
|
|||||||
ctx.user.ignoresUsers &&
|
ctx.user.ignoresUsers &&
|
||||||
ctx.user.ignoresUsers.length > 0
|
ctx.user.ignoresUsers.length > 0
|
||||||
) {
|
) {
|
||||||
comments = comments.where({
|
query.merge({
|
||||||
author_id: { $nin: ctx.user.ignoresUsers },
|
author_id: { $nin: ctx.user.ignoresUsers },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return executeWithSort(ctx, comments, { cursor, sortOrder, sortBy, limit });
|
return executeWithSort(ctx, query, { cursor, sortOrder, sortBy, limit });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+55
-18
@@ -1,23 +1,60 @@
|
|||||||
const SettingsService = require('../../services/settings');
|
const Settings = require('../../services/settings');
|
||||||
const DataLoader = require('dataloader');
|
const DataLoader = require('dataloader');
|
||||||
|
const { zipObject } = require('lodash');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a set of loaders based on a GraphQL context.
|
* SettingsLoader manages loading specific fields only of the Settings object.
|
||||||
* @param {Object} context the context of the GraphQL request
|
|
||||||
* @return {Object} object of loaders
|
|
||||||
*/
|
*/
|
||||||
module.exports = () => {
|
class SettingsLoader {
|
||||||
const loader = new DataLoader(selections =>
|
constructor() {
|
||||||
Promise.all(
|
this._loader = new DataLoader(this._batchLoadFn.bind(this));
|
||||||
selections.map(fields => {
|
this._cache = null;
|
||||||
return SettingsService.retrieve(fields);
|
}
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
async _batchLoadFn(fields) {
|
||||||
Settings: {
|
// Load a settings object with all the requested fields, unless we have the
|
||||||
load: (fields = false) => loader.load(fields),
|
// entire object cached, in which case we'll return the whole cache.
|
||||||
},
|
const obj = this._cache
|
||||||
};
|
? await this._cache
|
||||||
};
|
: await Settings.select(...fields);
|
||||||
|
|
||||||
|
// Return the specific fields for each of the fields that were loaded.
|
||||||
|
return fields.map(field => obj[field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load will return the entire Settings object with all fields.
|
||||||
|
*/
|
||||||
|
load() {
|
||||||
|
if (this._cache) {
|
||||||
|
// Return the cached settings promise.
|
||||||
|
return this._cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a promise that will return the settings object.
|
||||||
|
const promise = Settings.retrieve();
|
||||||
|
|
||||||
|
// Set this as the cached value.
|
||||||
|
this._cache = promise;
|
||||||
|
|
||||||
|
// Return the promised settings.
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* select will return a promise which resolves to the Settings object that
|
||||||
|
* contains the requested fields only.
|
||||||
|
*
|
||||||
|
* @param {Array<String>} fields the fields from Settings we want to load.
|
||||||
|
*/
|
||||||
|
async select(...fields) {
|
||||||
|
// Load all the values for the specific fields.
|
||||||
|
const values = await this._loader.loadMany(fields);
|
||||||
|
|
||||||
|
// Zip up the fields and values to create an object to return and return the
|
||||||
|
// assembled Settings object.
|
||||||
|
return zipObject(fields, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => ({ Settings: new SettingsLoader() });
|
||||||
|
|||||||
@@ -14,8 +14,15 @@ const getActionItem = async (ctx, { item_id, item_type }) => {
|
|||||||
const { loaders: { Comments, Users } } = ctx;
|
const { loaders: { Comments, Users } } = ctx;
|
||||||
|
|
||||||
switch (item_type) {
|
switch (item_type) {
|
||||||
case 'COMMENTS':
|
case 'COMMENTS': {
|
||||||
return Comments.get.load(item_id);
|
// Get a comment by ID, unless the comment is deleted, then return null.
|
||||||
|
const comment = await Comments.get.load(item_id);
|
||||||
|
if (comment.deleted_at) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
case 'USERS':
|
case 'USERS':
|
||||||
return Users.getByID.load(item_id);
|
return Users.getByID.load(item_id);
|
||||||
default:
|
default:
|
||||||
|
|||||||
+66
-36
@@ -1,4 +1,8 @@
|
|||||||
const { ErrNotFound, ErrNotAuthorized } = require('../../errors');
|
const {
|
||||||
|
ErrNotFound,
|
||||||
|
ErrNotAuthorized,
|
||||||
|
ErrPasswordIncorrect,
|
||||||
|
} = require('../../errors');
|
||||||
const Users = require('../../services/users');
|
const Users = require('../../services/users');
|
||||||
const migrationHelpers = require('../../services/migration/helpers');
|
const migrationHelpers = require('../../services/migration/helpers');
|
||||||
const {
|
const {
|
||||||
@@ -8,7 +12,7 @@ const {
|
|||||||
SET_USER_BAN_STATUS,
|
SET_USER_BAN_STATUS,
|
||||||
SET_USER_SUSPENSION_STATUS,
|
SET_USER_SUSPENSION_STATUS,
|
||||||
UPDATE_USER_ROLES,
|
UPDATE_USER_ROLES,
|
||||||
DELETE_USER,
|
DELETE_OTHER_USER,
|
||||||
CHANGE_PASSWORD,
|
CHANGE_PASSWORD,
|
||||||
} = require('../../perms/constants');
|
} = require('../../perms/constants');
|
||||||
|
|
||||||
@@ -66,15 +70,25 @@ const setRole = (ctx, id, role) => {
|
|||||||
/**
|
/**
|
||||||
* transforms a specific action to a removal action on the target model.
|
* transforms a specific action to a removal action on the target model.
|
||||||
*/
|
*/
|
||||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
const actionDecrTransformer = ({ item_id, action_type, group_id }) => {
|
||||||
query: { id: item_id },
|
const update = {
|
||||||
update: {
|
|
||||||
$inc: {
|
$inc: {
|
||||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
});
|
|
||||||
|
if (group_id) {
|
||||||
|
// If the action had a groupID, also decrement that key.
|
||||||
|
update.$inc[
|
||||||
|
`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`
|
||||||
|
] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: { id: item_id },
|
||||||
|
update,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// delUser will delete a given user with the specified id.
|
// delUser will delete a given user with the specified id.
|
||||||
const delUser = async (ctx, id) => {
|
const delUser = async (ctx, id) => {
|
||||||
@@ -93,7 +107,7 @@ const delUser = async (ctx, id) => {
|
|||||||
updateBatchSize: 10000,
|
updateBatchSize: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove all actions against comments.
|
// Remove all actions against this users comments.
|
||||||
await transformSingleWithCursor(
|
await transformSingleWithCursor(
|
||||||
Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||||
actionDecrTransformer,
|
actionDecrTransformer,
|
||||||
@@ -112,34 +126,50 @@ const delUser = async (ctx, id) => {
|
|||||||
.setOptions({ multi: true })
|
.setOptions({ multi: true })
|
||||||
.remove();
|
.remove();
|
||||||
|
|
||||||
// Removes all the user's reply counts on each of the comments that they
|
// Remove the user from all other user's ignore lists.
|
||||||
// have commented on.
|
await User.update(
|
||||||
|
{ ignoresUsers: user.id },
|
||||||
|
{
|
||||||
|
$pull: { ignoresUsers: user.id },
|
||||||
|
},
|
||||||
|
{ multi: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// For each comment that the user has authored, purge the comment data from it
|
||||||
|
// and unset their id from those comments.
|
||||||
await transformSingleWithCursor(
|
await transformSingleWithCursor(
|
||||||
Comment.collection.aggregate([
|
Comment.collection.find({ author_id: user.id }),
|
||||||
{ $match: { author_id: user.id } },
|
({
|
||||||
{
|
id,
|
||||||
$group: {
|
asset_id,
|
||||||
_id: '$parent_id',
|
status,
|
||||||
count: { $sum: 1 },
|
parent_id,
|
||||||
},
|
reply_count,
|
||||||
},
|
created_at,
|
||||||
]),
|
updated_at,
|
||||||
({ _id: parent_id, count }) => ({
|
}) => ({
|
||||||
query: { id: parent_id },
|
query: { id },
|
||||||
update: {
|
replace: {
|
||||||
$inc: {
|
id,
|
||||||
reply_count: -1 * count,
|
body: null,
|
||||||
},
|
body_history: [],
|
||||||
|
asset_id,
|
||||||
|
author_id: null,
|
||||||
|
status_history: [],
|
||||||
|
status,
|
||||||
|
parent_id,
|
||||||
|
reply_count,
|
||||||
|
action_counts: {},
|
||||||
|
tags: [],
|
||||||
|
metadata: {},
|
||||||
|
deleted_at: new Date(),
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Comment
|
Comment
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove all the user's comments.
|
|
||||||
await Comment.where({ author_id: user.id })
|
|
||||||
.setOptions({ multi: true })
|
|
||||||
.remove();
|
|
||||||
|
|
||||||
// Remove the user.
|
// Remove the user.
|
||||||
await user.remove();
|
await user.remove();
|
||||||
};
|
};
|
||||||
@@ -154,17 +184,17 @@ const changeUserPassword = async (ctx, oldPassword, newPassword) => {
|
|||||||
// Verify the old password.
|
// Verify the old password.
|
||||||
const validPassword = await user.verifyPassword(oldPassword);
|
const validPassword = await user.verifyPassword(oldPassword);
|
||||||
if (!validPassword) {
|
if (!validPassword) {
|
||||||
throw new ErrNotAuthorized();
|
throw new ErrPasswordIncorrect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the users password now.
|
// Change the users password now.
|
||||||
await Users.changePassword(user.id, newPassword);
|
await Users.changePassword(user.id, newPassword);
|
||||||
|
|
||||||
// Get some context for the email to be sent.
|
// Get some context for the email to be sent.
|
||||||
const { organizationName, organizationContactEmail } = await Settings.load([
|
const { organizationName, organizationContactEmail } = await Settings.select(
|
||||||
'organizationName',
|
'organizationName',
|
||||||
'organizationContactEmail',
|
'organizationContactEmail'
|
||||||
]);
|
);
|
||||||
|
|
||||||
// Send the password change email.
|
// Send the password change email.
|
||||||
await Users.sendEmail(user, {
|
await Users.sendEmail(user, {
|
||||||
@@ -225,7 +255,7 @@ module.exports = ctx => {
|
|||||||
setUserSuspensionStatus(ctx, id, until, message);
|
setUserSuspensionStatus(ctx, id, until, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.can(DELETE_USER)) {
|
if (ctx.user.can(DELETE_OTHER_USER)) {
|
||||||
mutators.User.del = id => delUser(ctx, id);
|
mutators.User.del = id => delUser(ctx, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { decorateWithTags } = require('./util');
|
const { decorateWithTags, getRequestedFields } = require('./util');
|
||||||
|
|
||||||
const Asset = {
|
const Asset = {
|
||||||
async comment({ id }, { id: commentId }, { loaders: { Comments } }) {
|
async comment({ id }, { id: commentId }, { loaders: { Comments } }) {
|
||||||
@@ -64,14 +64,17 @@ const Asset = {
|
|||||||
|
|
||||||
return Comments.countByAssetID.load(id);
|
return Comments.countByAssetID.load(id);
|
||||||
},
|
},
|
||||||
async settings({ settings = null }, _, { loaders: { Settings } }) {
|
async settings({ settings = null }, _, { loaders: { Settings } }, info) {
|
||||||
|
// Get the fields we want from the settings.
|
||||||
|
const fields = getRequestedFields(info);
|
||||||
|
|
||||||
// Load the global settings, and merge them into the asset specific settings
|
// Load the global settings, and merge them into the asset specific settings
|
||||||
// if we have some.
|
// if we have some.
|
||||||
let globalSettings = await Settings.load();
|
let globalSettings = await Settings.select(...fields);
|
||||||
if (settings !== null) {
|
if (settings !== null) {
|
||||||
settings = Object.assign({}, globalSettings.toObject(), settings);
|
settings = Object.assign({}, globalSettings, settings);
|
||||||
} else {
|
} else {
|
||||||
settings = globalSettings.toObject();
|
settings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const {
|
|||||||
SEARCH_ACTIONS,
|
SEARCH_ACTIONS,
|
||||||
SEARCH_COMMENT_STATUS_HISTORY,
|
SEARCH_COMMENT_STATUS_HISTORY,
|
||||||
VIEW_BODY_HISTORY,
|
VIEW_BODY_HISTORY,
|
||||||
|
VIEW_COMMENT_DELETED_AT,
|
||||||
} = require('../../perms/constants');
|
} = require('../../perms/constants');
|
||||||
const {
|
const {
|
||||||
decorateWithTags,
|
decorateWithTags,
|
||||||
@@ -23,7 +24,9 @@ const Comment = {
|
|||||||
return Comments.get.load(parent_id);
|
return Comments.get.load(parent_id);
|
||||||
},
|
},
|
||||||
user({ author_id }, _, { loaders: { Users } }) {
|
user({ author_id }, _, { loaders: { Users } }) {
|
||||||
return Users.getByID.load(author_id);
|
if (author_id) {
|
||||||
|
return Users.getByID.load(author_id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
|
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
|
||||||
// Don't bother looking up replies if there aren't any there!
|
// Don't bother looking up replies if there aren't any there!
|
||||||
@@ -55,10 +58,14 @@ const Comment = {
|
|||||||
return Assets.getByID.load(asset_id);
|
return Assets.getByID.load(asset_id);
|
||||||
},
|
},
|
||||||
async editing(comment, _, { loaders: { Settings } }) {
|
async editing(comment, _, { loaders: { Settings } }) {
|
||||||
const settings = await Settings.load();
|
const { editCommentWindowLength } = await Settings.select(
|
||||||
const editableUntil = new Date(
|
'editCommentWindowLength'
|
||||||
Number(new Date(comment.created_at)) + settings.editCommentWindowLength
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const editableUntil = new Date(
|
||||||
|
Number(new Date(comment.created_at)) + editCommentWindowLength
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
edited: comment.edited,
|
edited: comment.edited,
|
||||||
editableUntil: editableUntil,
|
editableUntil: editableUntil,
|
||||||
@@ -83,6 +90,7 @@ decorateWithTags(Comment);
|
|||||||
decorateWithPermissionCheck(Comment, {
|
decorateWithPermissionCheck(Comment, {
|
||||||
actions: [SEARCH_ACTIONS],
|
actions: [SEARCH_ACTIONS],
|
||||||
status_history: [SEARCH_COMMENT_STATUS_HISTORY],
|
status_history: [SEARCH_COMMENT_STATUS_HISTORY],
|
||||||
|
deleted_at: [VIEW_COMMENT_DELETED_AT],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protect privileged fields.
|
// Protect privileged fields.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { decorateWithPermissionCheck } = require('./util');
|
const { decorateWithPermissionCheck, getRequestedFields } = require('./util');
|
||||||
const {
|
const {
|
||||||
SEARCH_ASSETS,
|
SEARCH_ASSETS,
|
||||||
SEARCH_OTHERS_COMMENTS,
|
SEARCH_OTHERS_COMMENTS,
|
||||||
@@ -16,8 +16,12 @@ const RootQuery = {
|
|||||||
|
|
||||||
return Assets.getByURL(query.url);
|
return Assets.getByURL(query.url);
|
||||||
},
|
},
|
||||||
settings(_, args, { loaders: { Settings } }) {
|
settings(_, args, { loaders: { Settings } }, info) {
|
||||||
return Settings.load();
|
// Get the fields we want from the settings.
|
||||||
|
const fields = getRequestedFields(info);
|
||||||
|
|
||||||
|
// Load only the requested fields.
|
||||||
|
return Settings.select(...fields);
|
||||||
},
|
},
|
||||||
|
|
||||||
// This endpoint is used for loading moderation queues, so hide it in the
|
// This endpoint is used for loading moderation queues, so hide it in the
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ const {
|
|||||||
ADD_COMMENT_TAG,
|
ADD_COMMENT_TAG,
|
||||||
SEARCH_OTHER_USERS,
|
SEARCH_OTHER_USERS,
|
||||||
} = require('../../perms/constants');
|
} = require('../../perms/constants');
|
||||||
const { property, isBoolean } = require('lodash');
|
const { property, isBoolean, pull } = require('lodash');
|
||||||
|
const graphqlFields = require('graphql-fields');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getResolver will get the resolver from the typeResolver or apply the default
|
* getResolver will get the resolver from the typeResolver or apply the default
|
||||||
@@ -207,7 +208,11 @@ const decorateWithTags = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRequestedFields = info =>
|
||||||
|
pull(Object.keys(graphqlFields(info)), '__typename');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getRequestedFields,
|
||||||
decorateUserField,
|
decorateUserField,
|
||||||
decorateWithTags,
|
decorateWithTags,
|
||||||
decorateWithPermissionCheck,
|
decorateWithPermissionCheck,
|
||||||
|
|||||||
+14
-2
@@ -418,6 +418,9 @@ input CommentsQuery {
|
|||||||
|
|
||||||
# Exclude comments ignored by the requesting user
|
# Exclude comments ignored by the requesting user
|
||||||
excludeIgnored: Boolean
|
excludeIgnored: Boolean
|
||||||
|
|
||||||
|
# excludeDeleted when true will exclude deleted comments from the response.
|
||||||
|
excludeDeleted: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
input RepliesQuery {
|
input RepliesQuery {
|
||||||
@@ -434,6 +437,9 @@ input RepliesQuery {
|
|||||||
|
|
||||||
# Exclude comments ignored by the requesting user
|
# Exclude comments ignored by the requesting user
|
||||||
excludeIgnored: Boolean
|
excludeIgnored: Boolean
|
||||||
|
|
||||||
|
# excludeDeleted when true will exclude deleted comments from the response.
|
||||||
|
excludeDeleted: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
# CommentCountQuery allows the ability to query comment counts by specific
|
# CommentCountQuery allows the ability to query comment counts by specific
|
||||||
@@ -463,6 +469,9 @@ input CommentCountQuery {
|
|||||||
|
|
||||||
# Filter by a specific tag name.
|
# Filter by a specific tag name.
|
||||||
tags: [String!]
|
tags: [String!]
|
||||||
|
|
||||||
|
# excludeDeleted when true will exclude deleted comments from the count.
|
||||||
|
excludeDeleted: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
# UserCountQuery allows the ability to query user counts by specific
|
# UserCountQuery allows the ability to query user counts by specific
|
||||||
@@ -503,7 +512,7 @@ type Comment {
|
|||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
# The actual comment data.
|
# The actual comment data.
|
||||||
body: String!
|
body: String
|
||||||
|
|
||||||
# The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or
|
# The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or
|
||||||
# the author.
|
# the author.
|
||||||
@@ -519,7 +528,7 @@ type Comment {
|
|||||||
replies(query: RepliesQuery = {}): CommentConnection!
|
replies(query: RepliesQuery = {}): CommentConnection!
|
||||||
|
|
||||||
# replyCount is the number of replies with a depth of 1. Only direct replies
|
# replyCount is the number of replies with a depth of 1. Only direct replies
|
||||||
# to this comment are counted.
|
# to this comment are counted. Deleted comments are included in this count.
|
||||||
replyCount: Int
|
replyCount: Int
|
||||||
|
|
||||||
# Actions completed on the parent. Requires the `ADMIN` role.
|
# Actions completed on the parent. Requires the `ADMIN` role.
|
||||||
@@ -537,6 +546,9 @@ type Comment {
|
|||||||
# The status history of the comment. Requires the `ADMIN` or `MODERATOR` role.
|
# The status history of the comment. Requires the `ADMIN` or `MODERATOR` role.
|
||||||
status_history: [CommentStatusHistory!]
|
status_history: [CommentStatusHistory!]
|
||||||
|
|
||||||
|
# The date that the comment was deleted at if it was.
|
||||||
|
deleted_at: Date
|
||||||
|
|
||||||
# The time when the comment was created
|
# The time when the comment was created
|
||||||
created_at: Date!
|
created_at: Date!
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ en:
|
|||||||
embedlink:
|
embedlink:
|
||||||
copy: "Copy to Clipboard"
|
copy: "Copy to Clipboard"
|
||||||
error:
|
error:
|
||||||
|
PASSWORD_INCORRECT: "Your current password was entered incorrectly"
|
||||||
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
||||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||||
EMAIL_ALREADY_VERIFIED: "Email address already verified."
|
EMAIL_ALREADY_VERIFIED: "Email address already verified."
|
||||||
@@ -250,7 +251,9 @@ en:
|
|||||||
ALREADY_EXISTS: "Resource already exists"
|
ALREADY_EXISTS: "Resource already exists"
|
||||||
INVALID_ASSET_URL: "Assert URL is invalid"
|
INVALID_ASSET_URL: "Assert URL is invalid"
|
||||||
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
|
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
|
||||||
|
INCORRECT_PASSWORD: "Incorrect Password"
|
||||||
email: "Please enter a valid email."
|
email: "Please enter a valid email."
|
||||||
|
DELETION_NOT_SCHEDULED: "Deletion was not scheduled"
|
||||||
confirm_password: "Passwords don't match. Please check again"
|
confirm_password: "Passwords don't match. Please check again"
|
||||||
network_error: "Failed to connect to server. Check your internet connection and try again."
|
network_error: "Failed to connect to server. Check your internet connection and try again."
|
||||||
email_not_verified: "Email address {0} not verified."
|
email_not_verified: "Email address {0} not verified."
|
||||||
@@ -271,6 +274,7 @@ en:
|
|||||||
comment: comment
|
comment: comment
|
||||||
comment_is_ignored: "This comment is hidden because you ignored this user."
|
comment_is_ignored: "This comment is hidden because you ignored this user."
|
||||||
comment_is_rejected: "You have rejected this comment."
|
comment_is_rejected: "You have rejected this comment."
|
||||||
|
comment_is_deleted: "This commenter has deleted their account."
|
||||||
comment_is_hidden: "This comment is not available."
|
comment_is_hidden: "This comment is not available."
|
||||||
comments: comments
|
comments: comments
|
||||||
configure_stream: "Configure"
|
configure_stream: "Configure"
|
||||||
|
|||||||
+63
-61
@@ -16,26 +16,27 @@ es:
|
|||||||
send: "Enviar"
|
send: "Enviar"
|
||||||
notify_ban_headline: "Notificar al usuario de la suspensión"
|
notify_ban_headline: "Notificar al usuario de la suspensión"
|
||||||
notify_ban_description: "Esto notificará al usuario por email que fueron suspendidos de la comunidad"
|
notify_ban_description: "Esto notificará al usuario por email que fueron suspendidos de la comunidad"
|
||||||
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
|
email_message_ban: "Estimado/a {0},\n\nUsted o alguien con acceso a su cuenta a violado los lineamientos de nuestra comunidad. Como consecuencia de esto, su cuenta fue bloqueada. No podrá realizar ni reportar más comentarios. Si usted piensa que esto ha sido un error, por favor contáctese con nosotros."
|
||||||
bio_offensive: "Esta biografia es ofensiva"
|
bio_offensive: "Esta biografia es ofensiva"
|
||||||
cancel: Cancelar
|
cancel: Cancelar
|
||||||
confirm_email:
|
confirm_email:
|
||||||
click_to_confirm: "Click below to confirm your email address"
|
click_to_confirm: "Haga click debajo para confirmar su dirección de email"
|
||||||
confirm: "Confirm"
|
confirm: "Confirmar"
|
||||||
password_reset:
|
password_reset:
|
||||||
set_new_password: "Change Your Password"
|
mail_sent: 'Si tiene una cuenta registrada, un enlace para resetear su clave ha sido enviado a esa dirección de correo.'
|
||||||
new_password: "New Password"
|
set_new_password: "Cambie su contraseña"
|
||||||
new_password_help: "Password must be at least 8 characters"
|
new_password: "Nueva contraseña"
|
||||||
confirm_new_password: "Confirm New Password"
|
new_password_help: "La contraseña debe tener al menos 8 caracteres"
|
||||||
change_password: "Change Password"
|
confirm_new_password: "Confirme su nueva contraseña"
|
||||||
characters_remaining: "carácteres restantes"
|
change_password: "Cambio de contraseña"
|
||||||
|
characters_remaining: "caracteres restantes"
|
||||||
comment:
|
comment:
|
||||||
anon: Anónimo
|
anon: Anónimo
|
||||||
undo_reject: "Undo"
|
undo_reject: "Deshacer"
|
||||||
ban_user: "Usuario Suspendido"
|
ban_user: "Usuario Suspendido"
|
||||||
comment: "Publicar un comentario"
|
comment: "Publicar un comentario"
|
||||||
edited: Editado
|
edited: Editado
|
||||||
flagged: reportado
|
flagged: Reportado
|
||||||
view_context: "Ver contexto"
|
view_context: "Ver contexto"
|
||||||
comment_box:
|
comment_box:
|
||||||
post: "Publicar"
|
post: "Publicar"
|
||||||
@@ -62,16 +63,16 @@ es:
|
|||||||
reactions: 'reacciones'
|
reactions: 'reacciones'
|
||||||
story: 'Artículo'
|
story: 'Artículo'
|
||||||
flagged_usernames:
|
flagged_usernames:
|
||||||
notify_approved: '{0} approved username {1}'
|
notify_approved: '{0} ha aprobado el nombre de usuario {1}'
|
||||||
notify_rejected: '{0} rejected username {1}'
|
notify_rejected: '{0} ha rechazado el nombre de usuario {1}'
|
||||||
notify_flagged: '{0} reported username {1}'
|
notify_flagged: '{0} ha reportado el nombre de usuario {1}'
|
||||||
notify_changed: 'user {0} changed their username to {1}'
|
notify_changed: 'El usuario {0} ha modificado su nombre de usuario por {1}'
|
||||||
community:
|
community:
|
||||||
account_creation_date: "Fecha de creación de la cuenta"
|
account_creation_date: "Fecha de creación de la cuenta"
|
||||||
active: Activa
|
active: Activa
|
||||||
admin: Administrator
|
admin: Administrator
|
||||||
ads_marketing: "This looks like an ad/marketing"
|
ads_marketing: "Esto parece ser un ad/marketing"
|
||||||
are_you_sure: "¿Estas segura que quieres suspender a {0}?"
|
are_you_sure: "¿Estás segura que quieres suspender a {0}?"
|
||||||
ban_user: "¿Quieres suspender al Usuario?"
|
ban_user: "¿Quieres suspender al Usuario?"
|
||||||
banned: Suspendido
|
banned: Suspendido
|
||||||
banned_user: "Usuario Suspendido"
|
banned_user: "Usuario Suspendido"
|
||||||
@@ -201,10 +202,10 @@ es:
|
|||||||
minutes_plural: "minutos"
|
minutes_plural: "minutos"
|
||||||
email:
|
email:
|
||||||
suspended:
|
suspended:
|
||||||
subject: "Your account has been suspended"
|
subject: "Su cuenta ha sido suspendida"
|
||||||
banned:
|
banned:
|
||||||
subject: "Your account has been banned"
|
subject: "Su cuenta ha sido bloqueada"
|
||||||
body: "In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community."
|
body: "De acuerdo con las guías de comunidad de The Coral Project, su cuenta a sido bloqueada. No podrá hacer comentarios, reportar o entrar en contacto con nuestra comunidad."
|
||||||
confirm:
|
confirm:
|
||||||
has_been_requested: "Un correo de confirmación ha sido pedido para la siguiente cuenta:"
|
has_been_requested: "Un correo de confirmación ha sido pedido para la siguiente cuenta:"
|
||||||
to_confirm: "Para confirmar la cuenta, por favor visitar el siguiente enlace:"
|
to_confirm: "Para confirmar la cuenta, por favor visitar el siguiente enlace:"
|
||||||
@@ -220,8 +221,8 @@ es:
|
|||||||
copy: "Copiar al portapapeles"
|
copy: "Copiar al portapapeles"
|
||||||
error:
|
error:
|
||||||
COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe."
|
COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe."
|
||||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
EMAIL_VERIFICATION_TOKEN_INVALID: "El token de verificación de su cuenta de correo es inválido."
|
||||||
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
|
PASSWORD_RESET_TOKEN_INVALID: "El enlace para resetear su clave es inválido."
|
||||||
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
|
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
|
||||||
NOT_AUTHORIZED: "Acción no autorizada."
|
NOT_AUTHORIZED: "Acción no autorizada."
|
||||||
NO_SPECIAL_CHARACTERS: "Los nombres pueden contener letras números y _"
|
NO_SPECIAL_CHARACTERS: "Los nombres pueden contener letras números y _"
|
||||||
@@ -230,19 +231,20 @@ es:
|
|||||||
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
||||||
USERNAME_IN_USE: "Este nombre ya está siendo usado."
|
USERNAME_IN_USE: "Este nombre ya está siendo usado."
|
||||||
USERNAME_REQUIRED: "Debe ingresar un nombre"
|
USERNAME_REQUIRED: "Debe ingresar un nombre"
|
||||||
EMAIL_NOT_VERIFIED: "E-mail address not verified"
|
EMAIL_NOT_VERIFIED: "La cuenta de email no ha sido verificada."
|
||||||
EDIT_WINDOW_ENDED:: "No puedes editar este comentario. El tiempo de edición ha expirado."
|
EDIT_WINDOW_ENDED: "No puedes editar este comentario. El tiempo de edición ha expirado."
|
||||||
EDIT_USERNAME_NOT_AUTHORIZED: "No tiene permiso para editar el nombre de usuario."
|
EDIT_USERNAME_NOT_AUTHORIZED: "No tiene permiso para editar el nombre de usuario."
|
||||||
SAME_USERNAME_PROVIDED: "You must submit a different username."
|
SAME_USERNAME_PROVIDED: "Debe proporcinar un nombre de usuario diferente."
|
||||||
EMAIL_IN_USE: "Este correo se encuentra en uso"
|
EMAIL_IN_USE: "Este correo se encuentra en uso"
|
||||||
EMAIL_REQUIRED: "Se requiere un correo"
|
EMAIL_REQUIRED: "Se requiere un correo"
|
||||||
LOGIN_MAXIMUM_EXCEEDED: "Ha realizado demasiados intentos fallidos de usar la contraseña. Por favor espere."
|
LOGIN_MAXIMUM_EXCEEDED: "Ha realizado demasiados intentos fallidos de usar la contraseña. Por favor espere."
|
||||||
PASSWORD_REQUIRED: "Debe ingresar la contraseña"
|
PASSWORD_REQUIRED: "Debe ingresar la contraseña"
|
||||||
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
|
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
|
||||||
NOT_FOUND: "Recurso no encontrado"
|
NOT_FOUND: "Recurso no encontrado"
|
||||||
ALREADY_EXISTS: "Resource already exists"
|
ALREADY_EXISTS: "El recurso ya existe"
|
||||||
INVALID_ASSET_URL: "La URL del articulo no es valida"
|
INVALID_ASSET_URL: "La URL del articulo no es valida"
|
||||||
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
|
CANNOT_IGNORE_STAFF: "No puede ignorar a miembros del Staff."
|
||||||
|
INCORRECT_PASSWORD: "Contraseña Incorrecta"
|
||||||
email: "No es un correo válido"
|
email: "No es un correo válido"
|
||||||
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
|
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
|
||||||
network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo."
|
network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo."
|
||||||
@@ -253,8 +255,8 @@ es:
|
|||||||
password: "La contraseña debe tener por lo menos 8 caracteres"
|
password: "La contraseña debe tener por lo menos 8 caracteres"
|
||||||
username: "Los nombres pueden contener letras números y _"
|
username: "Los nombres pueden contener letras números y _"
|
||||||
required_field: "Este campo es requerido"
|
required_field: "Este campo es requerido"
|
||||||
unexpected: "Lo siento. Ha habido un error no previsto."
|
unexpected: "Lo siento. Ha ocurrido un error no previsto."
|
||||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
temporarily_suspended: "Su cuenta fue suspendida. Será reactivada {0}. Si tiene preguntas, por favor contáctese con nosotros."
|
||||||
flag_comment: "Reportar este comentario"
|
flag_comment: "Reportar este comentario"
|
||||||
flag_reason: "Razón por la que hacer este reporte (Opcional)"
|
flag_reason: "Razón por la que hacer este reporte (Opcional)"
|
||||||
flag_username: "Reportar el nombre de usuario"
|
flag_username: "Reportar el nombre de usuario"
|
||||||
@@ -274,7 +276,7 @@ es:
|
|||||||
label: "Nuevo Nombre"
|
label: "Nuevo Nombre"
|
||||||
msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta."
|
msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta."
|
||||||
changed_name:
|
changed_name:
|
||||||
msg: "Your username change is under review by our moderation team."
|
msg: "El cambio en su nombre de usuario está siendo revisado por nuestro equipo de moderación."
|
||||||
my_comments: "Mis Comentarios"
|
my_comments: "Mis Comentarios"
|
||||||
my_profile: "Mi perfil"
|
my_profile: "Mi perfil"
|
||||||
new_count: "Ver {0} más {1} "
|
new_count: "Ver {0} más {1} "
|
||||||
@@ -305,9 +307,9 @@ es:
|
|||||||
comment_spam: "Contiene spam"
|
comment_spam: "Contiene spam"
|
||||||
comment_noagree: "No está de acuerdo"
|
comment_noagree: "No está de acuerdo"
|
||||||
comment_other: "Otra razón"
|
comment_other: "Otra razón"
|
||||||
suspect_word: "Suspect Word"
|
suspect_word: "Palabra sospechosa"
|
||||||
banned_word: "Banned Word"
|
banned_word: "Palabra prohibida"
|
||||||
body_count: "Body exceeds max length"
|
body_count: "El texto exede el límite permitido"
|
||||||
trust: "Trust"
|
trust: "Trust"
|
||||||
links: "Link"
|
links: "Link"
|
||||||
modqueue:
|
modqueue:
|
||||||
@@ -315,14 +317,14 @@ es:
|
|||||||
actions: Acciones
|
actions: Acciones
|
||||||
all: todos
|
all: todos
|
||||||
all_streams: "Todos los Hilos"
|
all_streams: "Todos los Hilos"
|
||||||
notify_edited: '{0} edited comment "{1}"'
|
notify_edited: '{0} ha editado el comentario "{1}"'
|
||||||
notify_accepted: '{0} accepted comment "{1}"'
|
notify_accepted: '{0} ha aceptado el comentario "{1}"'
|
||||||
notify_rejected: '{0} rejected comment "{1}"'
|
notify_rejected: '{0} ha rechazado el comentario "{1}"'
|
||||||
notify_flagged: '{0} flagged comment "{1}"'
|
notify_flagged: '{0} ha reportado el comentario "{1}"'
|
||||||
notify_reset: '{0} reset status of comment "{1}"'
|
notify_reset: '{0} ha reseteado el status del comentario "{1}"'
|
||||||
approve: "Aprobar"
|
approve: "Aprobar"
|
||||||
approved: "Aprobado"
|
approved: "Aprobado"
|
||||||
ban_user: "Ban"
|
ban_user: "Bloquear"
|
||||||
billion: B
|
billion: B
|
||||||
close: Cerrar
|
close: Cerrar
|
||||||
empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️"
|
empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️"
|
||||||
@@ -362,7 +364,7 @@ es:
|
|||||||
no_agree_comment: "No estoy de acuerdo con este comentario"
|
no_agree_comment: "No estoy de acuerdo con este comentario"
|
||||||
no_like_bio: "No me gusta esta biografia"
|
no_like_bio: "No me gusta esta biografia"
|
||||||
no_like_username: "No me gusta este nombre de usuario"
|
no_like_username: "No me gusta este nombre de usuario"
|
||||||
already_flagged_username: "You have already flagged this username."
|
already_flagged_username: "Usted ya reportó a este usuario."
|
||||||
other: Otro
|
other: Otro
|
||||||
permalink: Compartir
|
permalink: Compartir
|
||||||
personal_info: "Este comentario muestra información personal"
|
personal_info: "Este comentario muestra información personal"
|
||||||
@@ -438,28 +440,28 @@ es:
|
|||||||
user_bio: "Bio de Usuario"
|
user_bio: "Bio de Usuario"
|
||||||
username_flags: "reportes para este nombre de usuario"
|
username_flags: "reportes para este nombre de usuario"
|
||||||
user_detail:
|
user_detail:
|
||||||
remove_suspension: "Remove Suspension"
|
remove_suspension: "Cancelar suspensión"
|
||||||
suspend: "Suspend User"
|
suspend: "Suspender usuario"
|
||||||
remove_ban: "Remove Ban"
|
remove_ban: "Cancelar bloqueo"
|
||||||
ban: "Ban User"
|
ban: "Bloquear usuario"
|
||||||
member_since: "Member Since"
|
member_since: "Miembro desde"
|
||||||
email: "Email"
|
email: "Email"
|
||||||
total_comments: "Total Comments"
|
total_comments: "Comentarios totales"
|
||||||
reject_rate: "Reject Rate"
|
reject_rate: "Reject Rate"
|
||||||
reports: "Reports"
|
reports: "Reportes"
|
||||||
all: "All"
|
all: "Todo"
|
||||||
rejected: "Rejected"
|
rejected: "Rechazar"
|
||||||
user_history: "User History"
|
user_history: "Historial del usuario"
|
||||||
user_history:
|
user_history:
|
||||||
user_banned: "User banned"
|
user_banned: "Usuario bloqueado"
|
||||||
ban_removed: "Ban removed"
|
ban_removed: "Bloqueo cancelado"
|
||||||
username_status: "Username {0}"
|
username_status: "Nombre de usuario {0}"
|
||||||
suspended: "Suspended, {0}"
|
suspended: "Suspendido, {0}"
|
||||||
suspension_removed: "Suspension removed"
|
suspension_removed: "Suspensión cancelada"
|
||||||
system: "System"
|
system: "Sistema"
|
||||||
date: "Date"
|
date: "Fecha"
|
||||||
action: "Action"
|
action: "Acción"
|
||||||
taken_by: "Taken By"
|
taken_by: "Está en uso"
|
||||||
user_impersonating: "Este usuario suplanta a alguien"
|
user_impersonating: "Este usuario suplanta a alguien"
|
||||||
user_no_comment: "No has dejado aún ningún comentario. ¡Únete a la conversación!"
|
user_no_comment: "No has dejado aún ningún comentario. ¡Únete a la conversación!"
|
||||||
username_offensive: "Este nombre de usuario es ofensivo"
|
username_offensive: "Este nombre de usuario es ofensivo"
|
||||||
@@ -488,5 +490,5 @@ es:
|
|||||||
launch: "Iniciar Talk"
|
launch: "Iniciar Talk"
|
||||||
close: "Cerrar este instalador"
|
close: "Cerrar este instalador"
|
||||||
admin_sidebar:
|
admin_sidebar:
|
||||||
view_options: "View Options"
|
view_options: "Ver opciones"
|
||||||
sort_comments: "Sort Comments"
|
sort_comments: "Ordenar comentarios"
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const createResolveFactory = (() => {
|
|||||||
module.exports = async (req, res, next) => {
|
module.exports = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
// Attach the custom css url.
|
// Attach the custom css url.
|
||||||
const { customCssUrl } = await SettingsService.retrieve('customCssUrl');
|
const { customCssUrl } = await SettingsService.select('customCssUrl');
|
||||||
res.locals.customCssUrl = customCssUrl;
|
res.locals.customCssUrl = customCssUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
|
|||||||
@@ -3,5 +3,9 @@ const uuid = require('uuid/v1');
|
|||||||
// Trace middleware attaches a request id to each incoming request.
|
// Trace middleware attaches a request id to each incoming request.
|
||||||
module.exports = (req, res, next) => {
|
module.exports = (req, res, next) => {
|
||||||
req.id = uuid();
|
req.id = uuid();
|
||||||
|
|
||||||
|
// Add the context ID to the request as an HTTP header.
|
||||||
|
res.set('X-Talk-Trace-ID', req.id);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ const Asset = new Schema(
|
|||||||
modified_date: Date,
|
modified_date: Date,
|
||||||
|
|
||||||
// This object is used exclusively for storing settings that are to override
|
// This object is used exclusively for storing settings that are to override
|
||||||
// the base settings from the base Settings object. This is to be accessed
|
// the base settings from the base Settings object.
|
||||||
// always after running `rectifySettings` against it.
|
|
||||||
settings: {
|
settings: {
|
||||||
default: {},
|
default: {},
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ const Comment = new Schema(
|
|||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
type: String,
|
type: String,
|
||||||
required: [true, 'The body is required.'],
|
|
||||||
minlength: 2,
|
|
||||||
},
|
},
|
||||||
body_history: [BodyHistoryItemSchema],
|
body_history: [BodyHistoryItemSchema],
|
||||||
asset_id: String,
|
asset_id: String,
|
||||||
@@ -89,6 +87,12 @@ const Comment = new Schema(
|
|||||||
// Tags are added by the self or by administrators.
|
// Tags are added by the self or by administrators.
|
||||||
tags: [TagLinkSchema],
|
tags: [TagLinkSchema],
|
||||||
|
|
||||||
|
// deleted_at stores the date that the given comment was deleted.
|
||||||
|
deleted_at: {
|
||||||
|
type: Date,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
// Additional metadata stored on the field.
|
// Additional metadata stored on the field.
|
||||||
metadata: {
|
metadata: {
|
||||||
default: {},
|
default: {},
|
||||||
|
|||||||
@@ -125,21 +125,4 @@ const Setting = new Schema(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges two settings objects.
|
|
||||||
*/
|
|
||||||
Setting.method('merge', function(src) {
|
|
||||||
Setting.eachPath(path => {
|
|
||||||
// Exclude internal fields...
|
|
||||||
if (['id', '_id', '__v', 'created_at', 'updated_at'].includes(path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the source object contains the path, shallow copy it.
|
|
||||||
if (path in src) {
|
|
||||||
this[path] = src[path];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Setting;
|
module.exports = Setting;
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "talk",
|
"name": "talk",
|
||||||
"version": "4.3.2",
|
"version": "4.4.0",
|
||||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"lint:js": "eslint bin/cli* .",
|
"lint:js": "eslint bin/cli* .",
|
||||||
"lint": "npm-run-all lint:*",
|
"lint": "npm-run-all lint:*",
|
||||||
"plugins:reconcile": "./bin/cli plugins reconcile",
|
"plugins:reconcile": "./bin/cli plugins reconcile",
|
||||||
"test": "npm-run-all test:jest test:mocha",
|
"test": "npm-run-all test:jest test:server:mocha",
|
||||||
"test:jest": "NODE_ENV=test jest --runInBand",
|
"test:jest": "NODE_ENV=test jest --runInBand",
|
||||||
"test:client": "NODE_ENV=test jest --projects client",
|
"test:client": "NODE_ENV=test jest --projects client",
|
||||||
"test:server": "npm-run-all test:server:jest test:server:mocha",
|
"test:server": "npm-run-all test:server:jest test:server:mocha",
|
||||||
@@ -79,11 +79,11 @@
|
|||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"babel-preset-es2015": "6.24.1",
|
"babel-preset-es2015": "6.24.1",
|
||||||
"babel-preset-react": "^6.23.0",
|
"babel-preset-react": "^6.23.0",
|
||||||
"bunyan-debug-stream": "^1.0.8",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bowser": "^1.7.2",
|
"bowser": "^1.7.2",
|
||||||
"brotli-webpack-plugin": "^0.5.0",
|
"brotli-webpack-plugin": "^0.5.0",
|
||||||
"bunyan": "^1.8.12",
|
"bunyan": "^1.8.12",
|
||||||
|
"bunyan-debug-stream": "^1.0.8",
|
||||||
"cli-table": "^0.3.1",
|
"cli-table": "^0.3.1",
|
||||||
"clipboard": "^1.7.1",
|
"clipboard": "^1.7.1",
|
||||||
"colors": "^1.1.2",
|
"colors": "^1.1.2",
|
||||||
@@ -98,7 +98,6 @@
|
|||||||
"dataloader": "^1.3.0",
|
"dataloader": "^1.3.0",
|
||||||
"debug": "3.1.0",
|
"debug": "3.1.0",
|
||||||
"dialog-polyfill": "^0.4.9",
|
"dialog-polyfill": "^0.4.9",
|
||||||
"did-you-mean": "^0.0.1",
|
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"ejs": "^2.5.7",
|
"ejs": "^2.5.7",
|
||||||
"env-rewrite": "^1.0.2",
|
"env-rewrite": "^1.0.2",
|
||||||
@@ -115,6 +114,7 @@
|
|||||||
"graphql-ast-tools": "0.2.3",
|
"graphql-ast-tools": "0.2.3",
|
||||||
"graphql-docs": "0.2.0",
|
"graphql-docs": "0.2.0",
|
||||||
"graphql-errors": "^2.1.0",
|
"graphql-errors": "^2.1.0",
|
||||||
|
"graphql-fields": "^1.0.2",
|
||||||
"graphql-redis-subscriptions": "1.3.0",
|
"graphql-redis-subscriptions": "1.3.0",
|
||||||
"graphql-subscriptions": "^0.4.3",
|
"graphql-subscriptions": "^0.4.3",
|
||||||
"graphql-tag": "^1.2.3",
|
"graphql-tag": "^1.2.3",
|
||||||
@@ -137,8 +137,8 @@
|
|||||||
"keymaster": "^1.6.2",
|
"keymaster": "^1.6.2",
|
||||||
"kue": "0.11.6",
|
"kue": "0.11.6",
|
||||||
"linkify-it": "^2.0.3",
|
"linkify-it": "^2.0.3",
|
||||||
"lodash": "^4.16.6",
|
"lodash": "^4.17.10",
|
||||||
"lodash-es": "^4.16.6",
|
"lodash-es": "^4.17.10",
|
||||||
"marked": "^0.3.6",
|
"marked": "^0.3.6",
|
||||||
"material-design-lite": "^1.2.1",
|
"material-design-lite": "^1.2.1",
|
||||||
"metascraper": "^3.9.2",
|
"metascraper": "^3.9.2",
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ module.exports = {
|
|||||||
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
|
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
|
||||||
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
||||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
||||||
DELETE_USER: 'DELETE_USER',
|
DELETE_OTHER_USER: 'DELETE_OTHER_USER',
|
||||||
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
|
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ module.exports = {
|
|||||||
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
|
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
|
||||||
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
|
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
|
||||||
VIEW_BODY_HISTORY: 'VIEW_BODY_HISTORY',
|
VIEW_BODY_HISTORY: 'VIEW_BODY_HISTORY',
|
||||||
|
VIEW_COMMENT_DELETED_AT: 'VIEW_COMMENT_DELETED_AT',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ module.exports = (user, perm) => {
|
|||||||
case types.UPDATE_USER_ROLES:
|
case types.UPDATE_USER_ROLES:
|
||||||
case types.CREATE_TOKEN:
|
case types.CREATE_TOKEN:
|
||||||
case types.REVOKE_TOKEN:
|
case types.REVOKE_TOKEN:
|
||||||
|
case types.DELETE_OTHER_USER:
|
||||||
return check(user, ['ADMIN']);
|
return check(user, ['ADMIN']);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module.exports = (user, perm) => {
|
|||||||
case types.VIEW_USER_ROLE:
|
case types.VIEW_USER_ROLE:
|
||||||
case types.VIEW_USER_EMAIL:
|
case types.VIEW_USER_EMAIL:
|
||||||
case types.VIEW_BODY_HISTORY:
|
case types.VIEW_BODY_HISTORY:
|
||||||
|
case types.VIEW_COMMENT_DELETED_AT:
|
||||||
return check(user, ['ADMIN', 'MODERATOR']);
|
return check(user, ['ADMIN', 'MODERATOR']);
|
||||||
case types.LIST_OWN_TOKENS:
|
case types.LIST_OWN_TOKENS:
|
||||||
return check(user, ['ADMIN']);
|
return check(user, ['ADMIN']);
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export {
|
|||||||
getErrorMessages,
|
getErrorMessages,
|
||||||
getDefinitionName,
|
getDefinitionName,
|
||||||
getShallowChanges,
|
getShallowChanges,
|
||||||
|
createDefaultResponseFragments,
|
||||||
} from 'coral-framework/utils';
|
} from 'coral-framework/utils';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { CREATE_MONGO_INDEXES } = require('../../../config');
|
|||||||
const Comment = require('models/comment');
|
const Comment = require('models/comment');
|
||||||
|
|
||||||
function getReactionConfig(reaction) {
|
function getReactionConfig(reaction) {
|
||||||
|
// Ensure that the reaction is a lowercase string.
|
||||||
reaction = reaction.toLowerCase();
|
reaction = reaction.toLowerCase();
|
||||||
|
|
||||||
if (CREATE_MONGO_INDEXES) {
|
if (CREATE_MONGO_INDEXES) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"server": [
|
"server": [
|
||||||
"talk-plugin-auth",
|
|
||||||
"talk-plugin-featured-comments",
|
"talk-plugin-featured-comments",
|
||||||
"talk-plugin-respect",
|
"talk-plugin-local-auth",
|
||||||
"talk-plugin-profile-data"
|
"talk-plugin-profile-data",
|
||||||
|
"talk-plugin-respect"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
"talk-plugin-auth",
|
"talk-plugin-auth",
|
||||||
@@ -11,15 +11,16 @@
|
|||||||
"talk-plugin-featured-comments",
|
"talk-plugin-featured-comments",
|
||||||
"talk-plugin-flag-details",
|
"talk-plugin-flag-details",
|
||||||
"talk-plugin-ignore-user",
|
"talk-plugin-ignore-user",
|
||||||
|
"talk-plugin-local-auth",
|
||||||
"talk-plugin-member-since",
|
"talk-plugin-member-since",
|
||||||
"talk-plugin-moderation-actions",
|
"talk-plugin-moderation-actions",
|
||||||
"talk-plugin-permalink",
|
"talk-plugin-permalink",
|
||||||
|
"talk-plugin-profile-data",
|
||||||
"talk-plugin-respect",
|
"talk-plugin-respect",
|
||||||
"talk-plugin-sort-most-replied",
|
"talk-plugin-sort-most-replied",
|
||||||
"talk-plugin-sort-most-respected",
|
"talk-plugin-sort-most-respected",
|
||||||
"talk-plugin-sort-newest",
|
"talk-plugin-sort-newest",
|
||||||
"talk-plugin-sort-oldest",
|
"talk-plugin-sort-oldest",
|
||||||
"talk-plugin-viewing-options",
|
"talk-plugin-viewing-options"
|
||||||
"talk-plugin-profile-data"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,9 @@ utilize our internal authentication system.
|
|||||||
|
|
||||||
To sync Talk auth with your own auth systems, you can use this plugin as a
|
To sync Talk auth with your own auth systems, you can use this plugin as a
|
||||||
template.
|
template.
|
||||||
|
|
||||||
|
## GDPR Compliance
|
||||||
|
|
||||||
|
In order to facilitate compliance with the
|
||||||
|
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
|
||||||
|
should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import SetUsernameDialog from './stream/containers/SetUsernameDialog';
|
|||||||
import translations from './translations.yml';
|
import translations from './translations.yml';
|
||||||
import Login from './login/containers/Main';
|
import Login from './login/containers/Main';
|
||||||
import reducer from './login/reducer';
|
import reducer from './login/reducer';
|
||||||
import ChangePassword from './profile-settings/containers/ChangePassword';
|
|
||||||
import ChangeUsername from './profile-settings/containers/ChangeUsername';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
reducer,
|
reducer,
|
||||||
@@ -13,7 +11,5 @@ export default {
|
|||||||
slots: {
|
slots: {
|
||||||
stream: [UserBox, SignInButton, SetUsernameDialog],
|
stream: [UserBox, SignInButton, SetUsernameDialog],
|
||||||
login: [Login],
|
login: [Login],
|
||||||
profileHeader: [ChangeUsername],
|
|
||||||
profileSettings: [ChangePassword],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styles from './ChangeUsername.css';
|
|
||||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
|
||||||
import ChangeUsernameDialog from './ChangeUsernameDialog';
|
|
||||||
import { t } from 'plugin-api/beta/client/services';
|
|
||||||
import InputField from './InputField';
|
|
||||||
import { getErrorMessages } from 'coral-framework/utils';
|
|
||||||
import { canUsernameBeUpdated } from 'coral-framework/utils/user';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
editing: false,
|
|
||||||
showDialog: false,
|
|
||||||
formData: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChangeUsername extends React.Component {
|
|
||||||
state = initialState;
|
|
||||||
|
|
||||||
clearForm = () => {
|
|
||||||
this.setState(initialState);
|
|
||||||
};
|
|
||||||
|
|
||||||
enableEditing = () => {
|
|
||||||
this.setState({
|
|
||||||
editing: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
disableEditing = () => {
|
|
||||||
this.setState({
|
|
||||||
editing: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cancel = () => {
|
|
||||||
this.clearForm();
|
|
||||||
this.disableEditing();
|
|
||||||
};
|
|
||||||
|
|
||||||
showDialog = () => {
|
|
||||||
this.setState({
|
|
||||||
showDialog: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSave = async () => {
|
|
||||||
this.showDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
saveChanges = async () => {
|
|
||||||
const { newUsername } = this.state.formData;
|
|
||||||
const { changeUsername } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await changeUsername(newUsername);
|
|
||||||
this.props.notify(
|
|
||||||
'success',
|
|
||||||
t('talk-plugin-auth.change_username.changed_username_success_msg')
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.props.notify('error', getErrorMessages(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearForm();
|
|
||||||
this.disableEditing();
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = e => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
|
|
||||||
this.setState(state => ({
|
|
||||||
formData: {
|
|
||||||
...state.formData,
|
|
||||||
[name]: value,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
closeDialog = () => {
|
|
||||||
this.setState({
|
|
||||||
showDialog: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
username,
|
|
||||||
emailAddress,
|
|
||||||
root: { me: { state: { status } } },
|
|
||||||
notify,
|
|
||||||
} = this.props;
|
|
||||||
const { editing, formData, showDialog } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={cn('talk-plugin-auth--edit-profile', styles.container, {
|
|
||||||
[styles.editing]: editing,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ChangeUsernameDialog
|
|
||||||
canUsernameBeUpdated={canUsernameBeUpdated(status)}
|
|
||||||
showDialog={showDialog}
|
|
||||||
onChange={this.onChange}
|
|
||||||
formData={formData}
|
|
||||||
username={username}
|
|
||||||
closeDialog={this.closeDialog}
|
|
||||||
saveChanges={this.saveChanges}
|
|
||||||
notify={notify}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{editing ? (
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.detailList}>
|
|
||||||
<InputField
|
|
||||||
icon="person"
|
|
||||||
id="newUsername"
|
|
||||||
name="newUsername"
|
|
||||||
onChange={this.onChange}
|
|
||||||
defaultValue={username}
|
|
||||||
columnDisplay
|
|
||||||
validationType="username"
|
|
||||||
>
|
|
||||||
<span className={styles.bottomText}>
|
|
||||||
{t('talk-plugin-auth.change_username.change_username_note')}
|
|
||||||
</span>
|
|
||||||
</InputField>
|
|
||||||
<InputField
|
|
||||||
icon="email"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
value={emailAddress}
|
|
||||||
validationType="username"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.content}>
|
|
||||||
<h2 className={styles.username}>{username}</h2>
|
|
||||||
{emailAddress ? (
|
|
||||||
<p className={styles.email}>{emailAddress}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{editing ? (
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Button
|
|
||||||
className={cn(styles.button, styles.saveButton)}
|
|
||||||
icon="save"
|
|
||||||
onClick={this.onSave}
|
|
||||||
disabled={
|
|
||||||
!this.state.formData.newUsername ||
|
|
||||||
this.state.formData.newUsername === username
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('talk-plugin-auth.change_username.save')}
|
|
||||||
</Button>
|
|
||||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
|
||||||
{t('talk-plugin-auth.change_username.cancel')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Button
|
|
||||||
className={styles.button}
|
|
||||||
icon="settings"
|
|
||||||
onClick={this.enableEditing}
|
|
||||||
>
|
|
||||||
{t('talk-plugin-auth.change_username.edit_profile')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangeUsername.propTypes = {
|
|
||||||
root: PropTypes.object.isRequired,
|
|
||||||
changeUsername: PropTypes.func.isRequired,
|
|
||||||
notify: PropTypes.func.isRequired,
|
|
||||||
username: PropTypes.string,
|
|
||||||
emailAddress: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChangeUsername;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { compose } from 'react-apollo';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { connect } from 'plugin-api/beta/client/hocs';
|
|
||||||
import ChangePassword from '../components/ChangePassword';
|
|
||||||
import { notify } from 'coral-framework/actions/notification';
|
|
||||||
import { withChangePassword } from 'plugin-api/beta/client/hocs';
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
|
||||||
|
|
||||||
export default compose(connect(null, mapDispatchToProps), withChangePassword)(
|
|
||||||
ChangePassword
|
|
||||||
);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { compose } from 'react-apollo';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { connect } from 'plugin-api/beta/client/hocs';
|
|
||||||
import ChangeUsername from '../components/ChangeUsername';
|
|
||||||
import { notify } from 'coral-framework/actions/notification';
|
|
||||||
import { withChangeUsername } from 'plugin-api/beta/client/hocs';
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
|
||||||
|
|
||||||
export default compose(connect(null, mapDispatchToProps), withChangeUsername)(
|
|
||||||
ChangeUsername
|
|
||||||
);
|
|
||||||
@@ -154,8 +154,20 @@ en:
|
|||||||
bottom_note: "Note: You will not be able to change your username again for 14 days"
|
bottom_note: "Note: You will not be able to change your username again for 14 days"
|
||||||
confirm_changes: "Confirm Changes"
|
confirm_changes: "Confirm Changes"
|
||||||
username_does_not_match: "Username does not match"
|
username_does_not_match: "Username does not match"
|
||||||
changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days."
|
cant_be_equal: "Your new {0} must be different to your current one"
|
||||||
change_username_attempt: "Username can't be updated. Usernames can be changed every 14 days"
|
change_username_attempt: "Username can't be updated. Usernames can be changed every 14 days"
|
||||||
|
change_email:
|
||||||
|
confirm_email_change: "Confirm Email Address Change"
|
||||||
|
description: "You are attempting to change your email address. Your new email address will be used for your login and to receive account notifications."
|
||||||
|
old_email: "Old Email Address"
|
||||||
|
new_email: "New Email Address"
|
||||||
|
enter_password: "Enter Password"
|
||||||
|
incorrect_password: "Incorrect Password"
|
||||||
|
confirm_change: "Confirm Change"
|
||||||
|
cancel: "Cancel"
|
||||||
|
change_email_msg: "Email Address Changed - Your email address has been successfully changed. This email address will now be used for signing in and email notifications."
|
||||||
|
changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days."
|
||||||
|
change_username_attempt: "Username can't be updated. Usernames can only be changed every 14 days."
|
||||||
de:
|
de:
|
||||||
talk-plugin-auth:
|
talk-plugin-auth:
|
||||||
login:
|
login:
|
||||||
|
|||||||
@@ -27,4 +27,10 @@ Configuration:
|
|||||||
or by visiting the
|
or by visiting the
|
||||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register)
|
[Creating an App ID](https://developers.facebook.com/docs/apps/register)
|
||||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||||
enabled.
|
enabled.
|
||||||
|
|
||||||
|
## GDPR Compliance
|
||||||
|
|
||||||
|
In order to facilitate compliance with the
|
||||||
|
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
|
||||||
|
should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.errorMsg {
|
||||||
|
color: #FA4643;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningIcon {
|
||||||
|
color: #FA4643;
|
||||||
|
}
|
||||||
+1
-1
@@ -77,4 +77,4 @@
|
|||||||
|
|
||||||
.warningIcon {
|
.warningIcon {
|
||||||
color: #FA4643;
|
color: #FA4643;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -43,7 +43,7 @@ const InputField = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
styles.detailItemContent,
|
styles.detailItemContent,
|
||||||
{ [styles.error]: hasError },
|
{ [styles.error]: hasError && showError },
|
||||||
{ [styles.disabled]: disabled }
|
{ [styles.disabled]: disabled }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -27,3 +27,9 @@ Configuration:
|
|||||||
- `TALK_GOOGLE_CLIENT_SECRET` (**required**) - The Google OAuth2 client ID for
|
- `TALK_GOOGLE_CLIENT_SECRET` (**required**) - The Google OAuth2 client ID for
|
||||||
your Google login web app. You can learn more about getting a Google Client
|
your Google login web app. You can learn more about getting a Google Client
|
||||||
ID at the [Google API Console](https://console.developers.google.com/apis/).
|
ID at the [Google API Console](https://console.developers.google.com/apis/).
|
||||||
|
|
||||||
|
## GDPR Compliance
|
||||||
|
|
||||||
|
In order to facilitate compliance with the
|
||||||
|
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
|
||||||
|
should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
|
||||||
|
|||||||
@@ -10,16 +10,22 @@ import { bindActionCreators } from 'redux';
|
|||||||
import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions';
|
import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions';
|
||||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||||
import { t } from 'plugin-api/beta/client/services';
|
import { t } from 'plugin-api/beta/client/services';
|
||||||
|
import { getErrorMessages } from 'coral-framework/utils';
|
||||||
|
|
||||||
class IgnoreUserConfirmationContainer extends React.Component {
|
class IgnoreUserConfirmationContainer extends React.Component {
|
||||||
ignoreUser = () => {
|
ignoreUser = async () => {
|
||||||
const { ignoreUser, notify, comment, closeMenu } = this.props;
|
const { ignoreUser, notify, comment, closeMenu } = this.props;
|
||||||
ignoreUser(comment.user.id).then(() => {
|
|
||||||
|
try {
|
||||||
|
await ignoreUser(comment.user.id);
|
||||||
notify(
|
notify(
|
||||||
'success',
|
'success',
|
||||||
t('talk-plugin-ignore-user.notify_success', comment.user.username)
|
t('talk-plugin-ignore-user.notify_success', comment.user.username)
|
||||||
);
|
);
|
||||||
});
|
} catch (err) {
|
||||||
|
notify('error', getErrorMessages(err));
|
||||||
|
}
|
||||||
|
|
||||||
closeMenu();
|
closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: talk-plugin-local-auth
|
||||||
|
permalink: /plugin/talk-plugin-local-auth/
|
||||||
|
layout: plugin
|
||||||
|
plugin:
|
||||||
|
name: talk-plugin-local-auth
|
||||||
|
default: true
|
||||||
|
provides:
|
||||||
|
- Client
|
||||||
|
- Server
|
||||||
|
---
|
||||||
|
|
||||||
|
This plugin will eventually contain all the local authentication code that is
|
||||||
|
responsible for creating, resetting, and managing accounts provided locally
|
||||||
|
through an email and password based login.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- *Email Change*: Allows users to change their existing email address on their account.
|
||||||
|
- *Local Account Association*: Allows users that have signed up with an external auth strategy (such as Google) the ability to associate a email address and password for login. **Note: Existing users with external authentication will be prompted to setup a local account when they sign in and when new users create an account.**
|
||||||
|
|
||||||
|
## GDPR Compliance
|
||||||
|
|
||||||
|
In order to facilitate compliance with the
|
||||||
|
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
|
||||||
|
should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@coralproject/eslint-config-talk/client"
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
.dialog {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 320px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: Helvetica,Helvetica Neue,Verdana,sans-serif;
|
||||||
|
color:#3B4A53;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin: 15px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 0;
|
||||||
|
margin: 20px 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.itemIcon {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> i.itemIcon {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: #787D80;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: transparent;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: normal;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #787D80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.proceed {
|
||||||
|
background-color: #3498DB;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: #FA4643;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Dialog } from 'plugin-api/beta/client/components/ui';
|
||||||
|
import validate from 'coral-framework/helpers/validate';
|
||||||
|
import { getErrorMessages } from 'coral-framework/utils';
|
||||||
|
import styles from './AddEmailAddressDialog.css';
|
||||||
|
import { t } from 'plugin-api/beta/client/services';
|
||||||
|
|
||||||
|
import AddEmailContent from './AddEmailContent';
|
||||||
|
import VerifyEmailAddress from './VerifyEmailAddress';
|
||||||
|
import EmailAddressAdded from './EmailAddressAdded';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
step: 0,
|
||||||
|
showErrors: false,
|
||||||
|
errors: {},
|
||||||
|
formData: {
|
||||||
|
emailAddress: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
confirmEmailAddress: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateRequired = v =>
|
||||||
|
v ? '' : t('talk-plugin-local-auth.add_email.required_field');
|
||||||
|
|
||||||
|
const validateRepeat = (key, msg) => (v, data) => (v !== data[key] ? msg : '');
|
||||||
|
|
||||||
|
const validateEmail = v =>
|
||||||
|
validateRequired(v) || !validate.email(v)
|
||||||
|
? t('talk-plugin-local-auth.add_email.invalid_email_address')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const validatePassword = v => validateRequired(v);
|
||||||
|
|
||||||
|
class AddEmailAddressDialog extends React.Component {
|
||||||
|
state = initialState;
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
emailAddress: validateEmail,
|
||||||
|
confirmPassword: validatePassword,
|
||||||
|
confirmEmailAddress: validateRepeat(
|
||||||
|
'emailAddress',
|
||||||
|
t('talk-plugin-local-auth.add_email.confirm_email_address')
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = e => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
this.setState(
|
||||||
|
state => ({
|
||||||
|
formData: {
|
||||||
|
...state.formData,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
validateField = (value, name) => {
|
||||||
|
const error = this.fields[name](value, this.state.formData);
|
||||||
|
if (error) {
|
||||||
|
this.addError({ [name]: error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.removeError(name);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
addError = err => {
|
||||||
|
this.setState(({ errors }) => ({
|
||||||
|
errors: { ...errors, ...err },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
let hasErrors = false;
|
||||||
|
Object.keys(this.state.formData).forEach(k => {
|
||||||
|
hasErrors = !this.validateField(this.state.formData[k], k) || hasErrors;
|
||||||
|
});
|
||||||
|
return !hasErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeError = errKey => {
|
||||||
|
this.setState(state => {
|
||||||
|
const { [errKey]: _, ...errors } = state.errors;
|
||||||
|
return {
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
showErrors = () => {
|
||||||
|
this.setState({
|
||||||
|
showErrors: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmChanges = async () => {
|
||||||
|
if (!this.validate()) {
|
||||||
|
this.showErrors();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { emailAddress, confirmPassword } = this.state.formData;
|
||||||
|
const { attachLocalAuth } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await attachLocalAuth({
|
||||||
|
email: emailAddress,
|
||||||
|
password: confirmPassword,
|
||||||
|
});
|
||||||
|
this.props.notify('success', 'Email Added!');
|
||||||
|
this.goToNextStep();
|
||||||
|
} catch (err) {
|
||||||
|
this.props.notify('error', getErrorMessages(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
goToNextStep = () => {
|
||||||
|
this.setState(({ step }) => ({
|
||||||
|
step: step + 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { errors, formData, showErrors, step } = this.state;
|
||||||
|
const { root: { settings } } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog className={styles.dialog} open={true}>
|
||||||
|
{step === 0 && (
|
||||||
|
<AddEmailContent
|
||||||
|
formData={formData}
|
||||||
|
errors={errors}
|
||||||
|
showErrors={showErrors}
|
||||||
|
confirmChanges={this.confirmChanges}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 1 &&
|
||||||
|
!settings.requireEmailConfirmation && (
|
||||||
|
<EmailAddressAdded done={() => {}} />
|
||||||
|
)}
|
||||||
|
{step === 1 &&
|
||||||
|
settings.requireEmailConfirmation && (
|
||||||
|
<VerifyEmailAddress
|
||||||
|
emailAddress={formData.emailAddress}
|
||||||
|
done={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEmailAddressDialog.propTypes = {
|
||||||
|
attachLocalAuth: PropTypes.func.isRequired,
|
||||||
|
notify: PropTypes.func.isRequired,
|
||||||
|
root: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddEmailAddressDialog;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './AddEmailAddressDialog.css';
|
||||||
|
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import InputField from './InputField';
|
||||||
|
import { t } from 'plugin-api/beta/client/services';
|
||||||
|
|
||||||
|
const AddEmailContent = ({
|
||||||
|
formData,
|
||||||
|
errors,
|
||||||
|
showErrors,
|
||||||
|
confirmChanges,
|
||||||
|
onChange,
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<h4 className={styles.title}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.content.title')}
|
||||||
|
</h4>
|
||||||
|
<p className={styles.description}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.content.description')}
|
||||||
|
</p>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
<li className={styles.item}>
|
||||||
|
<Icon name="done" className={styles.itemIcon} />
|
||||||
|
<span className={styles.text}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.content.item_1')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className={styles.item}>
|
||||||
|
<Icon name="done" className={styles.itemIcon} />
|
||||||
|
<span className={styles.text}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.content.item_2')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className={styles.item}>
|
||||||
|
<Icon name="done" className={styles.itemIcon} />
|
||||||
|
<span className={styles.text}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.content.item_3')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form autoComplete="off">
|
||||||
|
<InputField
|
||||||
|
id="emailAddress"
|
||||||
|
label={t('talk-plugin-local-auth.add_email.enter_email_address')}
|
||||||
|
name="emailAddress"
|
||||||
|
type="email"
|
||||||
|
onChange={onChange}
|
||||||
|
value={formData.emailAddress}
|
||||||
|
hasError={!!errors.emailAddress}
|
||||||
|
errorMsg={errors.emailAddress}
|
||||||
|
showError={showErrors}
|
||||||
|
columnDisplay
|
||||||
|
showSuccess={false}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
id="confirmEmailAddress"
|
||||||
|
label={t('talk-plugin-local-auth.add_email.confirm_email_address')}
|
||||||
|
name="confirmEmailAddress"
|
||||||
|
type="email"
|
||||||
|
onChange={onChange}
|
||||||
|
value={formData.confirmEmailAddress}
|
||||||
|
hasError={!!errors.confirmEmailAddress}
|
||||||
|
errorMsg={errors.confirmEmailAddress}
|
||||||
|
showError={showErrors}
|
||||||
|
columnDisplay
|
||||||
|
showSuccess={false}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
id="confirmPassword"
|
||||||
|
label={t('talk-plugin-local-auth.add_email.insert_password')}
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
onChange={onChange}
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
hasError={!!errors.confirmPassword}
|
||||||
|
errorMsg={errors.confirmPassword}
|
||||||
|
showError={showErrors}
|
||||||
|
columnDisplay
|
||||||
|
showSuccess={false}
|
||||||
|
/>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<a
|
||||||
|
className={cn(styles.button, styles.proceed)}
|
||||||
|
onClick={confirmChanges}
|
||||||
|
>
|
||||||
|
{t('talk-plugin-local-auth.add_email.add_email_address')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
AddEmailContent.propTypes = {
|
||||||
|
formData: PropTypes.object.isRequired,
|
||||||
|
errors: PropTypes.object.isRequired,
|
||||||
|
showErrors: PropTypes.bool.isRequired,
|
||||||
|
confirmChanges: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddEmailContent;
|
||||||
+1
-8
@@ -43,18 +43,11 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottomNote {
|
|
||||||
font-size: 0.9em;
|
|
||||||
line-height: 20px;
|
|
||||||
padding-top: 10px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottomActions {
|
.bottomActions {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usernamesChange {
|
.emailChange {
|
||||||
margin: 18px 0;
|
margin: 18px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './ChangeEmailContentDialog.css';
|
||||||
|
import InputField from './InputField';
|
||||||
|
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||||
|
import { t } from 'plugin-api/beta/client/services';
|
||||||
|
|
||||||
|
class ChangeEmailContentDialog extends React.Component {
|
||||||
|
state = {
|
||||||
|
showError: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
showError = () => {
|
||||||
|
this.setState({
|
||||||
|
showError: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmChanges = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.formHasError()) {
|
||||||
|
this.showError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.props.save();
|
||||||
|
this.props.next();
|
||||||
|
};
|
||||||
|
|
||||||
|
formHasError = () => this.props.hasError('confirmPassword');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className={styles.close} onClick={this.props.cancel}>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
<h1 className={styles.title}>
|
||||||
|
{t('talk-plugin-local-auth.change_email.confirm_email_change')}
|
||||||
|
</h1>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<p className={styles.description}>
|
||||||
|
{t('talk-plugin-local-auth.change_email.description')}
|
||||||
|
</p>
|
||||||
|
<div className={styles.emailChange}>
|
||||||
|
<span className={styles.item}>
|
||||||
|
{t('talk-plugin-local-auth.change_email.old_email')}:{' '}
|
||||||
|
{this.props.email}
|
||||||
|
</span>
|
||||||
|
<span className={styles.item}>
|
||||||
|
{t('talk-plugin-local-auth.change_email.new_email')}:{' '}
|
||||||
|
{this.props.formData.newEmail}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={this.confirmChanges}>
|
||||||
|
<InputField
|
||||||
|
id="confirmPassword"
|
||||||
|
label={t('talk-plugin-local-auth.change_email.enter_password')}
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
defaultValue=""
|
||||||
|
hasError={this.props.hasError('confirmPassword')}
|
||||||
|
errorMsg={this.props.getError('confirmPassword')}
|
||||||
|
showError={this.state.showError}
|
||||||
|
columnDisplay
|
||||||
|
/>
|
||||||
|
<div className={styles.bottomActions}>
|
||||||
|
<Button
|
||||||
|
className={styles.cancel}
|
||||||
|
onClick={this.props.cancel}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t('talk-plugin-local-auth.change_email.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button className={styles.confirmChanges} type="submit">
|
||||||
|
{t('talk-plugin-local-auth.change_email.confirm_change')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeEmailContentDialog.propTypes = {
|
||||||
|
save: PropTypes.func,
|
||||||
|
next: PropTypes.func,
|
||||||
|
cancel: PropTypes.func,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
formData: PropTypes.object,
|
||||||
|
email: PropTypes.string,
|
||||||
|
hasError: PropTypes.func,
|
||||||
|
getError: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangeEmailContentDialog;
|
||||||
+17
-9
@@ -1,22 +1,30 @@
|
|||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #202020;
|
color: #202020;
|
||||||
padding: 10px;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: solid 1px transparent;
|
border: solid 1px transparent;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
&.editing {
|
&.editing {
|
||||||
border-color: #979797;
|
padding: 10px;
|
||||||
background-color: #EDEDED;
|
background-color: #EDEDED;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: -6px;
|
||||||
right: 10px;
|
right: 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -24,18 +32,18 @@
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: #202020;
|
color: #202020;
|
||||||
margin: 0 0 20px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailBottomBox {
|
.detailBottomBox {
|
||||||
display: block;
|
display: block;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 280px;
|
width: 230px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailLink {
|
.detailLink {
|
||||||
color: #00538A;
|
color: #00538A;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -59,7 +67,7 @@
|
|||||||
> i {
|
> i {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #399ee2;
|
background-color: #399ee2;
|
||||||
color: white;
|
color: white;
|
||||||
+75
-34
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import styles from './ChangePassword.css';
|
import styles from './ChangePassword.css';
|
||||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
import { Button, BareButton } from 'plugin-api/beta/client/components/ui';
|
||||||
import validate from 'coral-framework/helpers/validate';
|
import validate from 'coral-framework/helpers/validate';
|
||||||
import errorMsj from 'coral-framework/helpers/error';
|
import errorMsj from 'coral-framework/helpers/error';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
@@ -14,7 +14,11 @@ const initialState = {
|
|||||||
editing: false,
|
editing: false,
|
||||||
showErrors: true,
|
showErrors: true,
|
||||||
errors: {},
|
errors: {},
|
||||||
formData: {},
|
formData: {
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmNewPassword: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class ChangePassword extends React.Component {
|
class ChangePassword extends React.Component {
|
||||||
@@ -45,7 +49,9 @@ class ChangePassword extends React.Component {
|
|||||||
const cond = this.state.formData[field] === this.state.formData[field2];
|
const cond = this.state.formData[field] === this.state.formData[field2];
|
||||||
if (!cond) {
|
if (!cond) {
|
||||||
this.addError({
|
this.addError({
|
||||||
[field2]: t('talk-plugin-auth.change_password.passwords_dont_match'),
|
[field2]: t(
|
||||||
|
'talk-plugin-local-auth.change_password.passwords_dont_match'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.removeError(field2);
|
this.removeError(field2);
|
||||||
@@ -56,7 +62,7 @@ class ChangePassword extends React.Component {
|
|||||||
fieldValidation = (value, type, name) => {
|
fieldValidation = (value, type, name) => {
|
||||||
if (!value.length) {
|
if (!value.length) {
|
||||||
this.addError({
|
this.addError({
|
||||||
[name]: t('talk-plugin-auth.change_password.required_field'),
|
[name]: t('talk-plugin-local-auth.change_password.required_field'),
|
||||||
});
|
});
|
||||||
} else if (!validate[type](value)) {
|
} else if (!validate[type](value)) {
|
||||||
this.addError({ [name]: errorMsj[type] });
|
this.addError({ [name]: errorMsj[type] });
|
||||||
@@ -103,7 +109,13 @@ class ChangePassword extends React.Component {
|
|||||||
this.setState(initialState);
|
this.setState(initialState);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSave = async () => {
|
onSave = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.isSubmitBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { oldPassword, newPassword } = this.state.formData;
|
const { oldPassword, newPassword } = this.state.formData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -113,7 +125,23 @@ class ChangePassword extends React.Component {
|
|||||||
});
|
});
|
||||||
this.props.notify(
|
this.props.notify(
|
||||||
'success',
|
'success',
|
||||||
t('talk-plugin-auth.change_password.changed_password_msg')
|
t('talk-plugin-local-auth.change_password.changed_password_msg')
|
||||||
|
);
|
||||||
|
this.clearForm();
|
||||||
|
this.disableEditing();
|
||||||
|
} catch (err) {
|
||||||
|
this.props.notify('error', getErrorMessages(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onForgotPassword = async () => {
|
||||||
|
const { root: { me: { email } } } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.props.forgotPassword(email);
|
||||||
|
this.props.notify(
|
||||||
|
'success',
|
||||||
|
t('talk-plugin-local-auth.change_password.forgot_password_sent')
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.props.notify('error', getErrorMessages(err));
|
this.props.notify('error', getErrorMessages(err));
|
||||||
@@ -135,19 +163,26 @@ class ChangePassword extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { editing, errors } = this.state;
|
const { editing, errors, showErrors } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn('talk-plugin-auth--change-password', styles.container, {
|
className={cn(
|
||||||
[styles.editing]: editing,
|
'talk-plugin-local-auth--change-password',
|
||||||
})}
|
styles.container,
|
||||||
|
{
|
||||||
|
[styles.editing]: editing,
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<h3 className={styles.title}>
|
<h3 className={styles.title}>
|
||||||
{t('talk-plugin-auth.change_password.change_password')}
|
{t('talk-plugin-local-auth.change_password.change_password')}
|
||||||
</h3>
|
</h3>
|
||||||
{editing && (
|
{editing ? (
|
||||||
<form className="talk-plugin-auth--change-password-form">
|
<form
|
||||||
|
className="talk-plugin-local-auth--change-password-form"
|
||||||
|
onSubmit={this.onSave}
|
||||||
|
>
|
||||||
<InputField
|
<InputField
|
||||||
id="oldPassword"
|
id="oldPassword"
|
||||||
label="Old Password"
|
label="Old Password"
|
||||||
@@ -157,11 +192,14 @@ class ChangePassword extends React.Component {
|
|||||||
value={this.state.formData.oldPassword}
|
value={this.state.formData.oldPassword}
|
||||||
hasError={this.hasError('oldPassword')}
|
hasError={this.hasError('oldPassword')}
|
||||||
errorMsg={errors['oldPassword']}
|
errorMsg={errors['oldPassword']}
|
||||||
showErrors
|
showError={showErrors}
|
||||||
>
|
>
|
||||||
<span className={styles.detailBottomBox}>
|
<span className={styles.detailBottomBox}>
|
||||||
<a className={styles.detailLink}>
|
<a
|
||||||
{t('talk-plugin-auth.change_password.forgot_password')}
|
className={styles.detailLink}
|
||||||
|
onClick={this.onForgotPassword}
|
||||||
|
>
|
||||||
|
{t('talk-plugin-local-auth.change_password.forgot_password')}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</InputField>
|
</InputField>
|
||||||
@@ -174,7 +212,7 @@ class ChangePassword extends React.Component {
|
|||||||
value={this.state.formData.newPassword}
|
value={this.state.formData.newPassword}
|
||||||
hasError={this.hasError('newPassword')}
|
hasError={this.hasError('newPassword')}
|
||||||
errorMsg={errors['newPassword']}
|
errorMsg={errors['newPassword']}
|
||||||
showErrors
|
showError={showErrors}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
id="confirmNewPassword"
|
id="confirmNewPassword"
|
||||||
@@ -185,28 +223,30 @@ class ChangePassword extends React.Component {
|
|||||||
value={this.state.formData.confirmNewPassword}
|
value={this.state.formData.confirmNewPassword}
|
||||||
hasError={this.hasError('confirmNewPassword')}
|
hasError={this.hasError('confirmNewPassword')}
|
||||||
errorMsg={errors['confirmNewPassword']}
|
errorMsg={errors['confirmNewPassword']}
|
||||||
showErrors
|
showError={showErrors}
|
||||||
/>
|
/>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button
|
||||||
|
className={cn(styles.button, styles.saveButton)}
|
||||||
|
icon="save"
|
||||||
|
type="submit"
|
||||||
|
disabled={this.isSubmitBlocked()}
|
||||||
|
>
|
||||||
|
{t('talk-plugin-local-auth.change_password.save')}
|
||||||
|
</Button>
|
||||||
|
<BareButton
|
||||||
|
type="button"
|
||||||
|
className={styles.cancelButton}
|
||||||
|
onClick={this.cancel}
|
||||||
|
>
|
||||||
|
{t('talk-plugin-local-auth.change_password.cancel')}
|
||||||
|
</BareButton>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
|
||||||
{editing ? (
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Button
|
|
||||||
className={cn(styles.button, styles.saveButton)}
|
|
||||||
icon="save"
|
|
||||||
onClick={this.onSave}
|
|
||||||
disabled={this.isSubmitBlocked()}
|
|
||||||
>
|
|
||||||
{t('talk-plugin-auth.change_password.save')}
|
|
||||||
</Button>
|
|
||||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
|
||||||
{t('talk-plugin-auth.change_password.cancel')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button className={styles.button} onClick={this.enableEditing}>
|
<Button className={styles.button} onClick={this.enableEditing}>
|
||||||
{t('talk-plugin-auth.change_password.edit')}
|
{t('talk-plugin-local-auth.change_password.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -217,6 +257,7 @@ class ChangePassword extends React.Component {
|
|||||||
|
|
||||||
ChangePassword.propTypes = {
|
ChangePassword.propTypes = {
|
||||||
changePassword: PropTypes.func.isRequired,
|
changePassword: PropTypes.func.isRequired,
|
||||||
|
forgotPassword: PropTypes.func.isRequired,
|
||||||
notify: PropTypes.func.isRequired,
|
notify: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
.close {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 14px;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #363636;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #6b6b6b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
color: #4C4C4D;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomNote {
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomActions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usernamesChange {
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel {
|
||||||
|
border: 1px solid #787d80;
|
||||||
|
background-color: transparent;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmChanges {
|
||||||
|
background-color: #3498DB;
|
||||||
|
border-color: #3498DB;
|
||||||
|
color: white;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3ba3ec;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-38
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cn from 'classnames';
|
import styles from './ChangeUsernameContentDialog.css';
|
||||||
import styles from './ChangeUsernameDialog.css';
|
|
||||||
import InputField from './InputField';
|
import InputField from './InputField';
|
||||||
import { Button, Dialog } from 'plugin-api/beta/client/components/ui';
|
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||||
import { t } from 'plugin-api/beta/client/services';
|
import { t } from 'plugin-api/beta/client/services';
|
||||||
|
|
||||||
class ChangeUsernameDialog extends React.Component {
|
class ChangeUsernameContentDialog extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
showError: false,
|
showError: false,
|
||||||
};
|
};
|
||||||
@@ -17,7 +16,9 @@ class ChangeUsernameDialog extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
confirmChanges = async () => {
|
confirmChanges = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.formHasError()) {
|
if (this.formHasError()) {
|
||||||
this.showError();
|
this.showError();
|
||||||
return;
|
return;
|
||||||
@@ -26,13 +27,13 @@ class ChangeUsernameDialog extends React.Component {
|
|||||||
if (!this.props.canUsernameBeUpdated) {
|
if (!this.props.canUsernameBeUpdated) {
|
||||||
this.props.notify(
|
this.props.notify(
|
||||||
'error',
|
'error',
|
||||||
t('talk-plugin-auth.change_username.change_username_attempt')
|
t('talk-plugin-local-auth.change_username.change_username_attempt')
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.props.saveChanges();
|
await this.props.save();
|
||||||
this.props.closeDialog();
|
this.props.next();
|
||||||
};
|
};
|
||||||
|
|
||||||
formHasError = () =>
|
formHasError = () =>
|
||||||
@@ -40,31 +41,28 @@ class ChangeUsernameDialog extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<div>
|
||||||
open={this.props.showDialog}
|
<span className={styles.close} onClick={this.props.cancel}>
|
||||||
className={cn(styles.dialog, 'talk-plugin-auth--edit-profile-dialog')}
|
|
||||||
>
|
|
||||||
<span className={styles.close} onClick={this.props.closeDialog}>
|
|
||||||
×
|
×
|
||||||
</span>
|
</span>
|
||||||
<h1 className={styles.title}>
|
<h1 className={styles.title}>
|
||||||
{t('talk-plugin-auth.change_username.confirm_username_change')}
|
{t('talk-plugin-local-auth.change_username.confirm_username_change')}
|
||||||
</h1>
|
</h1>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
{t('talk-plugin-auth.change_username.description')}
|
{t('talk-plugin-local-auth.change_username.description')}
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.usernamesChange}>
|
<div className={styles.usernamesChange}>
|
||||||
<span className={styles.item}>
|
<span className={styles.item}>
|
||||||
{t('talk-plugin-auth.change_username.old_username')}:{' '}
|
{t('talk-plugin-local-auth.change_username.old_username')}:{' '}
|
||||||
{this.props.username}
|
{this.props.username}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.item}>
|
<span className={styles.item}>
|
||||||
{t('talk-plugin-auth.change_username.new_username')}:{' '}
|
{t('talk-plugin-local-auth.change_username.new_username')}:{' '}
|
||||||
{this.props.formData.newUsername}
|
{this.props.formData.newUsername}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<form>
|
<form onSubmit={this.confirmChanges}>
|
||||||
<InputField
|
<InputField
|
||||||
id="confirmNewUsername"
|
id="confirmNewUsername"
|
||||||
label="Re-enter new username"
|
label="Re-enter new username"
|
||||||
@@ -74,7 +72,7 @@ class ChangeUsernameDialog extends React.Component {
|
|||||||
defaultValue=""
|
defaultValue=""
|
||||||
hasError={this.formHasError() && this.state.showError}
|
hasError={this.formHasError() && this.state.showError}
|
||||||
errorMsg={t(
|
errorMsg={t(
|
||||||
'talk-plugin-auth.change_username.username_does_not_match'
|
'talk-plugin-local-auth.change_username.username_does_not_match'
|
||||||
)}
|
)}
|
||||||
showError={this.state.showError}
|
showError={this.state.showError}
|
||||||
columnDisplay
|
columnDisplay
|
||||||
@@ -82,36 +80,37 @@ class ChangeUsernameDialog extends React.Component {
|
|||||||
validationType="username"
|
validationType="username"
|
||||||
>
|
>
|
||||||
<span className={styles.bottomNote}>
|
<span className={styles.bottomNote}>
|
||||||
{t('talk-plugin-auth.change_username.bottom_note')}
|
{t('talk-plugin-local-auth.change_username.bottom_note')}
|
||||||
</span>
|
</span>
|
||||||
</InputField>
|
</InputField>
|
||||||
|
<div className={styles.bottomActions}>
|
||||||
|
<Button
|
||||||
|
className={styles.cancel}
|
||||||
|
onClick={this.props.cancel}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t('talk-plugin-local-auth.change_username.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button className={styles.confirmChanges} type="submit">
|
||||||
|
{t('talk-plugin-local-auth.change_username.confirm_changes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div className={styles.bottomActions}>
|
|
||||||
<Button className={styles.cancel}>
|
|
||||||
{t('talk-plugin-auth.change_username.cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className={styles.confirmChanges}
|
|
||||||
onClick={this.confirmChanges}
|
|
||||||
>
|
|
||||||
{t('talk-plugin-auth.change_username.confirm_changes')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeUsernameDialog.propTypes = {
|
ChangeUsernameContentDialog.propTypes = {
|
||||||
saveChanges: PropTypes.func,
|
save: PropTypes.func,
|
||||||
closeDialog: PropTypes.func,
|
next: PropTypes.func,
|
||||||
showDialog: PropTypes.bool,
|
cancel: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
username: PropTypes.string,
|
|
||||||
formData: PropTypes.object,
|
formData: PropTypes.object,
|
||||||
|
username: PropTypes.string,
|
||||||
canUsernameBeUpdated: PropTypes.bool.isRequired,
|
canUsernameBeUpdated: PropTypes.bool.isRequired,
|
||||||
notify: PropTypes.func.isRequired,
|
notify: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChangeUsernameDialog;
|
export default ChangeUsernameContentDialog;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.dialog {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 320px;
|
||||||
|
top: 10px;
|
||||||
|
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './ConfirmChangesDialog.css';
|
||||||
|
import { Dialog } from 'plugin-api/beta/client/components/ui';
|
||||||
|
|
||||||
|
const initialState = { step: 0 };
|
||||||
|
|
||||||
|
class ConfirmChangesDialog extends React.Component {
|
||||||
|
state = initialState;
|
||||||
|
|
||||||
|
goToNextStep = () => {
|
||||||
|
this.setState(({ step }) => ({
|
||||||
|
step: step + 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
clear = () => {
|
||||||
|
this.setState(initialState);
|
||||||
|
};
|
||||||
|
|
||||||
|
cancel = () => {
|
||||||
|
this.clear();
|
||||||
|
this.props.closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
continue = () => {
|
||||||
|
this.goToNextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
finish = () => {
|
||||||
|
this.clear();
|
||||||
|
this.props.closeDialog();
|
||||||
|
this.props.finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
renderSteps = () => {
|
||||||
|
const steps = React.Children.toArray(this.props.children)
|
||||||
|
.filter(child => child.props.enable)
|
||||||
|
.filter((_, i) => i === this.state.step);
|
||||||
|
|
||||||
|
return steps.map(child => {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
goToNextStep: this.goToNextStep,
|
||||||
|
clear: this.clear,
|
||||||
|
cancel: this.cancel,
|
||||||
|
next:
|
||||||
|
this.state.step === steps.length - 1 ? this.finish : this.continue,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={this.props.showDialog}
|
||||||
|
className={cn(
|
||||||
|
styles.dialog,
|
||||||
|
'talk-plugin-local-auth--edit-profile-dialog'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{this.renderSteps()}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmChangesDialog.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
closeDialog: PropTypes.func,
|
||||||
|
showDialog: PropTypes.bool,
|
||||||
|
finish: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmChangesDialog;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './AddEmailAddressDialog.css';
|
||||||
|
import { t } from 'plugin-api/beta/client/services';
|
||||||
|
|
||||||
|
const EmailAddressAdded = ({ done }) => (
|
||||||
|
<div>
|
||||||
|
<h4 className={styles.title}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.added.title')}
|
||||||
|
</h4>
|
||||||
|
<p className={styles.description}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.added.description')}
|
||||||
|
</p>
|
||||||
|
<strong>{t('talk-plugin-local-auth.add_email.added.subtitle')}</strong>
|
||||||
|
<p className={styles.description}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.added.description_2')}{' '}
|
||||||
|
<strong>{t('talk-plugin-local-auth.add_email.added.path')}</strong>.
|
||||||
|
</p>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<a className={cn(styles.button, styles.proceed)} onClick={done}>
|
||||||
|
{t('talk-plugin-local-auth.add_email.done')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
EmailAddressAdded.propTypes = {
|
||||||
|
done: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailAddressAdded;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './ErrorMessage.css';
|
||||||
|
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||||
|
|
||||||
|
const ErrorMessage = ({ children }) => (
|
||||||
|
<div className={styles.errorMsg}>
|
||||||
|
<Icon className={styles.warningIcon} name="warning" />
|
||||||
|
<span>{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ErrorMessage.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorMessage;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
.detailItem {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailItemContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnDisplay {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.detailItemMessage {
|
||||||
|
padding: 4px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailItemContent {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailInput {
|
||||||
|
border: solid 1px #787D80;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: white;
|
||||||
|
height: 30px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 230px;
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .detailIcon {
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #787D80;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border: solid 2px #FA4643;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background-color: #E0E0E0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
color: #4C4C4D;
|
||||||
|
font-size: 1em;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #000;
|
||||||
|
outline: none;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailItemMessage {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 6px;
|
||||||
|
|
||||||
|
.warningIcon, .checkIcon {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkIcon {
|
||||||
|
color: #00CD73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningIcon {
|
||||||
|
color: #FA4643;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import styles from './InputField.css';
|
||||||
|
import ErrorMessage from './ErrorMessage';
|
||||||
|
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||||
|
|
||||||
|
const InputField = ({
|
||||||
|
id = '',
|
||||||
|
label = '',
|
||||||
|
type = 'text',
|
||||||
|
name = '',
|
||||||
|
onChange = () => {},
|
||||||
|
showError = true,
|
||||||
|
hasError = false,
|
||||||
|
errorMsg = '',
|
||||||
|
children,
|
||||||
|
columnDisplay = false,
|
||||||
|
showSuccess = false,
|
||||||
|
validationType = '',
|
||||||
|
icon = '',
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const inputValue = {
|
||||||
|
...(value !== undefined ? { value } : {}),
|
||||||
|
...(defaultValue !== undefined ? { defaultValue } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<div className={cn(styles.detailItemContainer)}>
|
||||||
|
{label && (
|
||||||
|
<label className={styles.detailLabel} id={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(styles.detailItemContent, {
|
||||||
|
[styles.columnDisplay]: columnDisplay,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.detailInput,
|
||||||
|
{ [styles.error]: hasError && showError },
|
||||||
|
{ [styles.disabled]: disabled }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon && <Icon name={icon} className={styles.detailIcon} />}
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
className={styles.detailValue}
|
||||||
|
onChange={onChange}
|
||||||
|
autoComplete="off"
|
||||||
|
data-validation-type={validationType}
|
||||||
|
disabled={disabled}
|
||||||
|
{...inputValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItemMessage}>
|
||||||
|
{!hasError &&
|
||||||
|
showSuccess &&
|
||||||
|
value && (
|
||||||
|
<Icon className={styles.checkIcon} name="check_circle" />
|
||||||
|
)}
|
||||||
|
{hasError && showError && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputField.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
label: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
value: PropTypes.string,
|
||||||
|
defaultValue: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
showError: PropTypes.bool,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
errorMsg: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
columnDisplay: PropTypes.bool,
|
||||||
|
showSuccess: PropTypes.bool,
|
||||||
|
validationType: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputField;
|
||||||
+24
-12
@@ -1,36 +1,48 @@
|
|||||||
.container {
|
.container {
|
||||||
margin-bottom: 20px;
|
margin-top: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #202020;
|
color: #202020;
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
&.editing {
|
&.editing {
|
||||||
|
padding: 10px;
|
||||||
background-color: #EDEDED;
|
background-color: #EDEDED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: inherit;
|
||||||
|
justify-content: inherit;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email {
|
.email {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
|
margin-top: 0;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
border: 1px solid #787d80;
|
border: 1px solid #787d80;
|
||||||
@@ -48,7 +60,7 @@
|
|||||||
> i {
|
> i {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #399ee2;
|
background-color: #399ee2;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -82,13 +94,13 @@
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 230px;
|
width: 230px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
> .detailLabelIcon {
|
> .detailLabelIcon {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
color: #787D80;
|
color: #787D80;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
@@ -115,7 +127,7 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailItem {
|
.detailItem {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user