Compare commits

..
93 Commits
Author SHA1 Message Date
Kim Gardner 4cca800af0 Merge pull request #1623 from coralproject/wyattjoh-patch-1
Bump to 4.4.1
2018-05-15 13:32:49 -04:00
Wyatt Johnson ee0790dbe8 Update package.json 2018-05-15 11:25:58 -06:00
Kim Gardner 6890d24e47 Merge pull request #1616 from coralproject/gdpr-change-email-bug
GDPR Bug - Confirm Email Dialog inputs shouldn't be connected to the save button in the background
2018-05-15 12:54:39 -04:00
okbel 2392e1ca78 Merge branch 'gdpr-change-email-bug' of github.com:coralproject/talk into gdpr-change-email-bug
* 'gdpr-change-email-bug' of github.com:coralproject/talk:
  Set is_test to false
  Added support for new indexes
2018-05-15 13:34:12 -03:00
okbel 40a431b374 Ready 2018-05-15 13:31:29 -03:00
Kim Gardner 5214705f5b Merge branch 'master' into gdpr-change-email-bug 2018-05-15 12:24:47 -04:00
Kim Gardner 79df9b487f Merge pull request #1622 from icco/patch-1
Set Akismet is_test to false
2018-05-15 12:24:23 -04:00
Nat Welch ef5e5ddbcf Set is_test to false
Akismet is no longer in test mode.
2018-05-15 11:55:02 -04:00
Wyatt Johnson 2e773a4438 Merge branch 'master' into gdpr-change-email-bug 2018-05-15 09:27:09 -06:00
Kim Gardner 2a2eabfae0 Merge pull request #1621 from coralproject/indexes
MongoDB Indexes
2018-05-15 11:07:55 -04:00
okbel bb9be71a7a Merge branch 'gdpr-change-email-bug' of github.com:coralproject/talk into gdpr-change-email-bug
* 'gdpr-change-email-bug' of github.com:coralproject/talk:
  Remove unused variable 2
  Remove unused variable
  Remove ModerationIndicator from container
  Show username already exists error
  No indicator on new queue
  Exclude deleted when loading more
  Remove modqueue counts on the frontend
2018-05-15 12:07:15 -03:00
okbel 50acf9ca50 Removing 2018-05-15 11:47:07 -03:00
Kim Gardner 3ae0136f75 Merge branch 'master' into gdpr-change-email-bug 2018-05-14 17:59:16 -04:00
Wyatt Johnson d3ad486f81 Merge branch 'master' into indexes 2018-05-14 14:52:57 -06:00
Wyatt Johnson 32510db1fa Merge pull request #1618 from coralproject/remove-mod-count-frontend
Remove modqueue counts on the frontend
2018-05-14 14:52:43 -06:00
Wyatt Johnson 8d44525c16 Added support for new indexes 2018-05-14 14:37:27 -06:00
Wyatt Johnson 83fddfdc40 Merge branch 'master' into remove-mod-count-frontend 2018-05-14 14:36:42 -06:00
Kim Gardner ed32a69b68 Merge branch 'master' into gdpr-change-email-bug 2018-05-14 16:32:51 -04:00
Kim Gardner 02b1e03c21 Merge pull request #1620 from coralproject/username-already-exists
Show username already exists error
2018-05-14 16:29:19 -04:00
Chi Vinh Le 08d523a4d6 Remove unused variable 2 2018-05-14 21:38:30 +02:00
Chi Vinh Le b5adea4b2a Remove unused variable 2018-05-14 21:30:09 +02:00
Chi Vinh Le beef4e8b3b Remove ModerationIndicator from container 2018-05-14 21:28:48 +02:00
Chi Vinh Le 5a190839d1 Show username already exists error 2018-05-14 20:44:30 +02:00
Chi Vinh Le ab7e5bc858 No indicator on new queue 2018-05-14 20:05:18 +02:00
Chi Vinh Le 12e848aace Exclude deleted when loading more 2018-05-14 19:27:18 +02:00
Chi Vinh Le 67c3fe3c5b Remove modqueue counts on the frontend 2018-05-14 19:18:34 +02:00
okbel 1f7c4e58a1 sepatate state 2018-05-14 13:31:24 -03:00
Wyatt Johnson f846ef0c06 Merge pull request #1595 from clintabrown/fix-my-profile-title-link
Fix My Profile story title link behavior
2018-05-11 14:37:23 -06:00
Wyatt Johnson fa31541fb7 Merge branch 'master' into fix-my-profile-title-link 2018-05-11 14:13:29 -06:00
Wyatt Johnson 4fcd5dd4f7 Merge pull request #1604 from coralproject/email-attach-fix
Email Attach Fixes
2018-05-11 13:05:45 -06:00
Chi Vinh Le 7d8d5a6115 Use lodash get 2018-05-11 20:46:32 +02:00
Chi Vinh Le f0dcd844a7 Fix excludeIf condition 2018-05-11 20:44:45 +02:00
Wyatt Johnson 9a1b9fb864 Merge branch 'master' into email-attach-fix 2018-05-11 11:39:08 -06:00
Wyatt Johnson 6cf4203c9b Update translations.yml 2018-05-11 11:38:54 -06:00
Kiwi 85e21f8090 Merge pull request #1606 from coralproject/ios-focus-zoom-issues
Disable autofocus on IOS Safari, prevent zoom on narrow screens
2018-05-11 14:31:21 -03:00
Chi Vinh Le 64583fabbe Merge branch 'email-attach-fix' of ssh://github.com/coralproject/talk into email-attach-fix 2018-05-11 18:40:26 +02:00
Kiwi bdaf5c633e Merge branch 'master' into email-attach-fix 2018-05-11 13:40:00 -03:00
Kiwi 945e2670f5 Merge branch 'master' into ios-focus-zoom-issues 2018-05-11 13:39:52 -03:00
Kiwi 4166e36e1e Merge pull request #1599 from coralproject/css-templates
CSS and Template Fixes + Improvements
2018-05-11 13:39:36 -03:00
Chi Vinh Le f018ccf33e Prevent showing add email address dialog when usernam is not set 2018-05-11 18:38:41 +02:00
Belén Curcio e61f325e86 Merge branch 'master' into fix-my-profile-title-link 2018-05-11 13:36:12 -03:00
Wyatt Johnson ef72b7b1d5 Merge branch 'master' into email-attach-fix 2018-05-11 10:33:32 -06:00
okbel a571cf830d Adding target 2018-05-11 13:32:55 -03:00
Kiwi 5dea051427 Merge branch 'master' into css-templates 2018-05-11 13:25:52 -03:00
Kim Gardner 730ef1a5c4 Merge pull request #1608 from mkonikov/copy-fix
Update download page copy
2018-05-11 12:25:39 -04:00
Belén Curcio 622b79b491 Removing unnecessary functions 2018-05-11 13:22:48 -03:00
Wyatt Johnson faa0ee8ceb added global wrap 2018-05-11 10:22:45 -06:00
Kiwi d67c25a791 Merge branch 'master' into email-attach-fix 2018-05-11 13:21:20 -03:00
Belén Curcio 32d615527a Merge branch 'master' into fix-my-profile-title-link 2018-05-11 13:08:13 -03:00
Wyatt Johnson 3a9051bf1b Merge branch 'master' into css-templates 2018-05-11 10:07:59 -06:00
Kim Gardner 44c2d7847a Merge branch 'master' into copy-fix 2018-05-11 11:55:53 -04:00
Kiwi 93359d8250 Merge pull request #1613 from coralproject/wyattjoh-patch-1
Delete package-lock.json
2018-05-11 12:37:28 -03:00
Wyatt Johnson ba2985e891 Merge branch 'master' into email-attach-fix 2018-05-11 09:33:55 -06:00
Wyatt Johnson 42d4abf842 review updates 2018-05-11 09:33:29 -06:00
Wyatt Johnson 29ffe287cd Delete package-lock.json 2018-05-11 09:21:55 -06:00
Kiwi 05c13126ad Merge branch 'master' into ios-focus-zoom-issues 2018-05-11 12:21:15 -03:00
Kim Gardner 787dd8ec8a Merge branch 'master' into copy-fix 2018-05-11 11:18:22 -04:00
Kim Gardner 47c7125190 Merge pull request #1612 from mkonikov/capitalize
Capitalize "Information"
2018-05-11 11:17:29 -04:00
Chi Vinh Le fc5621a060 Add reference to story 2018-05-11 17:12:28 +02:00
Mendel Konikov f57f8397fe Capitalize "Information" 2018-05-11 11:01:15 -04:00
Mendel Konikov 2d1b675234 Update download page copy 2018-05-10 21:01:26 -04:00
Kim Gardner ac7bea209e Merge branch 'master' into email-attach-fix 2018-05-10 18:53:39 -04:00
Kim Gardner dd1b51e09b Merge branch 'master' into fix-my-profile-title-link 2018-05-10 18:52:56 -04:00
Chi Vinh Le 631206f360 Disable autofocus on IOS Safari, prevent zoom on narrow screens 2018-05-11 00:30:12 +02:00
Wyatt Johnson 4d6ef95416 Merge branch 'master' into css-templates 2018-05-10 14:55:25 -06:00
Wyatt Johnson 462f5e8206 Merge pull request #1603 from coralproject/losowsky-patch-2
Corrections and clarity
2018-05-10 14:55:12 -06:00
Wyatt Johnson 524aa3e863 Merge branch 'master' into losowsky-patch-2 2018-05-10 14:46:09 -06:00
Andrew Losowsky e0cada5bde Update 03-07-product-guide-trust.md 2018-05-10 13:44:03 -07:00
Wyatt Johnson dd0381d22a Merge branch 'master' into email-attach-fix 2018-05-10 14:43:37 -06:00
Wyatt Johnson 2e37f78705 Email Attach Fixes 2018-05-10 13:31:00 -06:00
Kim Gardner 0203a65656 Merge branch 'master' into fix-my-profile-title-link 2018-05-10 15:23:24 -04:00
Kim Gardner 4413e5616c Merge pull request #1596 from coralproject/tombstone-copy
Small tweak to tombstone copy
2018-05-10 15:23:07 -04:00
Wyatt Johnson b9c5024e68 Merge branch 'master' into css-templates 2018-05-10 12:11:48 -06:00
Kim Gardner 38f9c2f458 Merge branch 'master' into tombstone-copy 2018-05-10 13:42:26 -04:00
Kim Gardner fc14c5ab1c Merge pull request #1598 from coralproject/mobile-fix
Fit Delete My Account modals on mobile
2018-05-10 13:42:06 -04:00
Wyatt Johnson 75f9a697cb Merge branch 'master' into tombstone-copy 2018-05-10 11:34:11 -06:00
Kim Gardner 89f87dc9ad Merge branch 'master' into css-templates 2018-05-10 13:33:50 -04:00
Kim Gardner 85f45f390c Merge branch 'master' into mobile-fix 2018-05-10 13:33:15 -04:00
Kim Gardner 10317671ee Merge branch 'master' into losowsky-patch-2 2018-05-10 10:41:01 -04:00
Kim Gardner b16f15e47d Merge pull request #1601 from coralproject/check-correct-email
Allow changing email back and forth
2018-05-10 10:40:34 -04:00
Andrew Losowsky 2554b26110 Corrections and clarity
Fixed the incorrect thresholds and made the description clearer
2018-05-10 10:21:44 -04:00
Chi Vinh Le a092a197cf Check correct email 2018-05-09 23:45:53 +02:00
Kim Gardner e069d823a4 Merge pull request #1600 from coralproject/cli-fix
CLI Fixes
2018-05-09 16:50:59 -04:00
Wyatt Johnson ca3d56def8 asset refresh patched 2018-05-09 14:40:24 -06:00
Wyatt Johnson 9af3772087 Added json output for assets list
- Fixes #1597
2018-05-09 14:28:04 -06:00
Wyatt Johnson 51525de679 fixed set-role description 2018-05-09 14:17:43 -06:00
Wyatt Johnson 969206f905 copy adjustment 2018-05-09 14:14:48 -06:00
Wyatt Johnson 302cab8ca0 added header to graphiql 2018-05-09 14:12:27 -06:00
Wyatt Johnson 55a9c1e6b4 added link to graphiql 2018-05-09 14:05:16 -06:00
Wyatt Johnson 4426be5f39 improved dev experience, fixed css order 2018-05-09 13:56:05 -06:00
okbel 0ded5c72e9 adding media 2018-05-09 14:23:22 -03:00
Kim Gardner 1517611c9e Update tombstone copy 2018-05-09 11:00:02 -04:00
Clint Brown 8be4300ff4 Fix My Profile story title link behavior 2018-05-09 17:41:16 +10:00
61 changed files with 620 additions and 427 deletions
+52 -23
View File
@@ -23,23 +23,33 @@ util.onshutdown([() => mongoose.disconnect()]);
/**
* Lists all the assets registered in the database.
*/
async function listAssets() {
async function listAssets(opts) {
try {
let assets = await AssetModel.find({}).sort({ created_at: 1 });
let table = new Table({
head: ['ID', 'Title', 'URL'],
});
switch (opts.format) {
case 'json': {
console.log(JSON.stringify(assets, null, 2));
break;
}
default: {
let table = new Table({
head: ['ID', 'Title', 'URL'],
});
assets.forEach(asset => {
table.push([
asset.id,
asset.title ? asset.title : '',
asset.url ? asset.url : '',
]);
});
assets.forEach(asset => {
table.push([
asset.id,
asset.title ? asset.title : '',
asset.url ? asset.url : '',
]);
});
console.log(table.toString());
break;
}
}
console.log(table.toString());
util.shutdown();
} catch (e) {
console.error(e);
@@ -49,12 +59,13 @@ async function listAssets() {
async function refreshAssets(ageString) {
try {
const now = new Date().getTime();
const ageMs = parseDuration(ageString);
const age = new Date(now - ageMs);
const query = AssetModel.find({}, { id: 1 });
if (ageString) {
// 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: [
{
scraped: {
@@ -65,16 +76,28 @@ async function refreshAssets(ageString) {
scraped: null,
},
],
},
{ id: 1 }
);
});
}
// Create a graph context.
const ctx = Context.forSystem();
// Load the assets.
const cursor = query.cursor();
// Queue all the assets for scraping.
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
console.log('Assets were queued to be scraped');
const promises = [];
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();
} catch (e) {
console.error(e);
@@ -202,11 +225,17 @@ async function rewrite(search, replace, options) {
program
.command('list')
.option(
'--format <type>',
'Specify the output format [table]',
/^(table|json)$/i,
'table'
)
.description('list all the assets in the database')
.action(listAssets);
program
.command('refresh <age>')
.command('refresh [age]')
.description('queues the assets that exceed the age requested')
.action(refreshAssets);
+1 -1
View File
@@ -328,7 +328,7 @@ program
.action(searchUsers);
program
.command('set-role <userID> <role>')
.command('set-role <userID>')
.description('sets the role on a user')
.action(setUserRole);
+11
View File
@@ -0,0 +1,11 @@
:global {
html, body, #root, #root > div {
min-height: 100%;
}
body {
margin: 0;
background-color: #FAFAFA;
font-family: 'Roboto', sans-serif;
}
}
+1
View File
@@ -1,5 +1,6 @@
import React from 'react';
import ToastContainer from './ToastContainer';
import './App.css';
import 'material-design-lite';
import AppRouter from '../AppRouter';
@@ -7,7 +7,6 @@ import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
@@ -32,7 +31,6 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.moderate')}
<ModerationIndicator root={root} data={data} />
</IndexLink>
)}
<Link
@@ -1,10 +1,8 @@
.indicator {
display: inline-block;
background-color: #E46D59;
border-radius: 10px;
position: absolute;
top: 50%;
width: 7px;
height: 7px;
margin-top: -4px;
margin-left: 7px;
}
+4 -5
View File
@@ -2,21 +2,20 @@ import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/Header';
import CommunityIndicator from '../routes/Community/containers/Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
// TODO: eventually we will readd modqueue counts
// import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import { getDefinitionName } from 'coral-framework/utils';
export default withQuery(
gql`
query TalkAdmin_Header($nullID: ID) {
...${getDefinitionName(ModerationIndicator.fragments.root)}
query TalkAdmin_Header {
...${getDefinitionName(CommunityIndicator.fragments.root)}
}
${ModerationIndicator.fragments.root}
${CommunityIndicator.fragments.root}
`,
{
options: {
variables: { nullID: null },
// variables: { nullID: null },
},
}
)(Header);
@@ -63,10 +63,6 @@ class Moderation extends Component {
this.props.toggleStorySearch(true);
};
getActiveTabCount = (props = this.props) => {
return props.root[`${props.activeTab}Count`];
};
moderate = accept => {
const {
acceptComment,
@@ -139,12 +135,14 @@ class Moderation extends Component {
const comments = root[activeTab];
const activeTabCount = this.getActiveTabCount();
const menuItems = Object.keys(queueConfig).map(queue => ({
key: queue,
name: queueConfig[queue].name,
icon: queueConfig[queue].icon,
count: root[`${queue}Count`],
indicator:
['premod', 'reported'].includes(queue) && root[queue].nodes.length > 0,
// TODO: Eventually we'll reintroduce counting
// count: root[`${props.queue}Count`]
}));
const slotPassthrough = {
@@ -189,7 +187,6 @@ class Moderation extends Component {
loadMore={this.loadMore}
commentBelongToQueue={this.props.commentBelongToQueue}
isLoadingMore={this.state.isLoadingMore}
commentCount={activeTabCount}
currentUserId={this.props.currentUser.id}
viewUserDetail={viewUserDetail}
selectCommentId={props.selectCommentId}
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import CountBadge from '../../../components/CountBadge';
import Indicator from '../../../components/Indicator';
import styles from './ModerationMenu.css';
import { Icon } from 'coral-ui';
import { Link } from 'react-router';
@@ -32,7 +32,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
activeClassName={styles.active}
>
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name}{' '}
<CountBadge count={queue.count} />
{queue.indicator && <Indicator />}
</Link>
))}
</div>
@@ -204,7 +204,7 @@ class ModerationQueue extends React.Component {
}
componentDidUpdate(prev) {
const { commentCount, selectedCommentId } = this.props;
const { selectedCommentId, hasNextPage } = this.props;
const switchedToMultiMode = prev.singleView && !this.props.singleView;
const switchedMode = prev.singleView !== this.props.singleView;
@@ -212,7 +212,6 @@ class ModerationQueue extends React.Component {
prev.selectedCommentId !== selectedCommentId && selectedCommentId;
const moderatedLastComment =
prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0;
const hasMoreComment = commentCount > 0;
if (switchedToMultiMode) {
// Reflow virtual list.
@@ -223,7 +222,7 @@ class ModerationQueue extends React.Component {
this.scrollToSelectedComment();
}
if (moderatedLastComment && hasMoreComment) {
if (moderatedLastComment && hasNextPage) {
this.props.loadMore();
}
}
@@ -240,10 +239,7 @@ class ModerationQueue extends React.Component {
const index = view.findIndex(
({ id }) => id === this.props.selectedCommentId
);
if (
index === view.length - 1 &&
this.getCommentCountWithoutDagling() !== this.props.commentCount
) {
if (index === view.length - 1 && this.props.hasNextPage) {
await this.props.loadMore();
this.selectDown();
return;
@@ -467,7 +463,6 @@ ModerationQueue.propTypes = {
acceptComment: PropTypes.func.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
cleanUpQueue: PropTypes.func.isRequired,
commentCount: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
singleView: PropTypes.bool,
isLoadingMore: PropTypes.bool,
@@ -314,11 +314,11 @@ class ModerationContainer extends Component {
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
if (premodEnabled && root.newCount === 0) {
if (premodEnabled && root.new.nodes.length === 0) {
delete currentQueueConfig.new;
}
if (!premodEnabled && root.premodCount === 0) {
if (!premodEnabled && root.premod.nodes.length === 0) {
delete currentQueueConfig.premod;
}
@@ -402,7 +402,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags, excludeDeleted: true}) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
@@ -456,7 +456,11 @@ const withModQueueQuery = withQuery(
}
`
)}
${Object.keys(queueConfig).map(
${
''
/*
TODO: eventually we'll reintroduce counting..
Object.keys(queueConfig).map(
queue => `
${queue}Count: commentCount(query: {
excludeDeleted: true,
@@ -478,7 +482,8 @@ const withModQueueQuery = withQuery(
asset_id: $asset_id,
})
`
)}
)*/
}
asset(id: $asset_id) @skip(if: $allAssets) {
id
title
@@ -31,6 +31,7 @@
font-weight: bold;
font-size: 12px;
color: #757575;
cursor: pointer;
}
.commentSummary {
@@ -11,16 +11,6 @@ import { getTotalReactionsCount } from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
class Comment extends React.Component {
goToStory = () => {
this.props.navigate(this.props.comment.asset.url);
};
goToConversation = () => {
this.props.navigate(
`${this.props.comment.asset.url}?commentId=${this.props.comment.id}`
);
};
render() {
const { comment, root } = this.props;
const reactionCount = getTotalReactionsCount(comment.action_summaries);
@@ -76,8 +66,8 @@ class Comment extends React.Component {
<div className="my-comment-asset">
<a
className={cn(styles.assetURL, 'my-comment-anchor')}
href="#"
onClick={this.goToStory}
href={this.props.comment.asset.url}
target="_parent"
>
{t('common.story')}:{' '}
{comment.asset.title ? comment.asset.title : comment.asset.url}
@@ -87,7 +77,13 @@ class Comment extends React.Component {
<div className={styles.sidebar}>
<ul>
<li>
<a onClick={this.goToConversation} className={styles.viewLink}>
<a
className={styles.viewLink}
href={`${this.props.comment.asset.url}?commentId=${
this.props.comment.id
}`}
target="_parent"
>
<Icon name="open_in_new" className={styles.iconView} />
{t('view_conversation')}
</a>
+11 -5
View File
@@ -37,7 +37,8 @@ export default class Popup extends Component {
this.onBlur();
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
// Use `onunload` instead of `onbeforeunload` which is not supported in iOS
// Safari.
this.ref.onunload = () => {
this.onUnload();
@@ -46,10 +47,15 @@ export default class Popup extends Component {
}
this.resetCallbackInterval = setInterval(() => {
if (this.ref && this.ref.onload === null) {
clearInterval(this.resetCallbackInterval);
this.resetCallbackInterval = null;
this.setCallbacks();
try {
if (this.ref && this.ref.onload === null) {
clearInterval(this.resetCallbackInterval);
this.resetCallbackInterval = null;
this.setCallbacks();
}
} catch (err) {
// We could be getting a security exception here if the login page
// gets redirected to another domain to authenticate.
}
}, 50);
@@ -59,6 +59,7 @@ const withSetUsername = hoistStatics(WrappedComponent => {
}
const changeSet = { success: false, loading: false, error };
this.setState(changeSet);
throw error;
}
};
+25 -19
View File
@@ -3,47 +3,53 @@ title: Trust
permalink: /trust/
---
Trust is a set of components within Talk that incorporate automated moderation
features based on a user's previous behavior.
Trust is a set of components within Talk that incorporate basic automated moderation features based on a user's previous behavior.
## User Karma Score
Using Trusts calculations, Talk will automatically pre-moderate comments of
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.
Using Trusts 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.)
When a commenter has one comment rejected, their next comment must be moderated
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.
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.
Here are the default thresholds:
```text
-2 and lower: Unreliable
-1 to +2: Neutral
+3 and higher: Reliable
-1 and lower: Unreliable
0 to +1: Neutral
+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
configuration.
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.
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
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,
which is accessed by clicking on a users name in the Admin.
which is accessed by clicking on a users 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
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
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.
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".
+12
View File
@@ -55,6 +55,18 @@ class SettingsLoader {
// assembled Settings object.
return zipObject(fields, values);
}
/**
* get, like select, will retrieve the settings, but get will only return a
* single setting.
*
* @param {String} field the field to get
*/
async get(field) {
const value = await this._loader.load(field);
return value;
}
}
module.exports = () => ({ Settings: new SettingsLoader() });
+2 -2
View File
@@ -57,8 +57,8 @@ const Comment = {
asset({ asset_id }, _, { loaders: { Assets } }) {
return Assets.getByID.load(asset_id);
},
async editing(comment, _, { loaders: { Settings } }) {
const { editCommentWindowLength } = await Settings.select(
editing: async (comment, _, { loaders: { Settings } }) => {
const editCommentWindowLength = await Settings.get(
'editCommentWindowLength'
);
+2 -2
View File
@@ -162,7 +162,7 @@ en:
suspect_word_title: "Suspect words list"
suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
tech_settings: "Tech Settings"
organization_information: "Organization information"
organization_information: "Organization Information"
organization_info_copy: "We use this information in email notifications generated by Talk. This connects the messages to your organization, and provides a way for users to contact you if they have an issue with their account."
organization_info_copy_2: "We recommend creating a generic email account (eg. community@yournewsroom.com) for this purpose. This means it can remain consistent over time, and doesn't expose a name that users could target if their account were blocked."
organization_details: "Organization Details"
@@ -274,7 +274,7 @@ en:
comment: comment
comment_is_ignored: "This comment is hidden because you ignored this user."
comment_is_rejected: "You have rejected this comment."
comment_is_deleted: "This comment was deleted."
comment_is_deleted: "This commenter has deleted their account."
comment_is_hidden: "This comment is not available."
comments: comments
configure_stream: "Configure"
+6 -2
View File
@@ -94,9 +94,13 @@ const createResolveFactory = (() => {
module.exports = async (req, res, next) => {
try {
// Attach the custom css url.
const { customCssUrl } = await SettingsService.select('customCssUrl');
// Attach the custom css url and organization name.
const { customCssUrl, organizationName } = await SettingsService.select(
'customCssUrl',
'organizationName'
);
res.locals.customCssUrl = customCssUrl;
res.locals.organizationName = organizationName;
} catch (err) {
console.warn(err);
}
+6 -13
View File
@@ -9,7 +9,8 @@ const Action = new Schema(
id: {
type: String,
default: uuid.v4,
unique: true,
unique: 1,
index: 1,
},
action_type: {
type: String,
@@ -19,7 +20,10 @@ const Action = new Schema(
type: String,
enum: ITEM_TYPES,
},
item_id: String,
item_id: {
type: String,
index: 1,
},
user_id: String,
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
@@ -37,15 +41,4 @@ const Action = new Schema(
}
);
// Create an index on the `item_id` field so that queries looking for
// actions based on the item id can resolve faster.
Action.index(
{
item_id: 1,
},
{
background: true,
}
);
module.exports = Action;
+55 -104
View File
@@ -55,12 +55,16 @@ const Comment = new Schema(
type: String,
default: uuid.v4,
unique: true,
index: true,
},
body: {
type: String,
},
body_history: [BodyHistoryItemSchema],
asset_id: String,
asset_id: {
type: String,
index: true,
},
author_id: String,
status_history: [Status],
status: {
@@ -90,7 +94,6 @@ const Comment = new Schema(
// deleted_at stores the date that the given comment was deleted.
deleted_at: {
type: Date,
default: null,
},
// Additional metadata stored on the field.
@@ -110,95 +113,67 @@ const Comment = new Schema(
}
);
// Add the indexes for the id of the comment.
Comment.index(
{
id: 1,
},
{
unique: true,
background: false,
}
);
Comment.index(
{
status: 1,
created_at: 1,
},
{
background: true,
}
);
Comment.index(
{
status: 1,
created_at: 1,
asset_id: 1,
},
{
background: true,
}
);
// Create a sparse index to search across.
Comment.index(
{
created_at: 1,
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Create a sparse index to search across.
Comment.index(
{
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for finding flagged comments.
Comment.index(
{
asset_id: 1,
created_at: 1,
'action_counts.flag': 1,
},
{
background: true,
}
);
// Add an index for the reply sort.
Comment.index(
{
asset_id: 1,
deleted_at: 1,
created_at: -1,
reply_count: -1,
},
{
background: true,
}
{ partialFilterExpression: { deleted_at: null } }
);
// Add an index that is optimized for finding a user's comments.
Comment.index(
{
author_id: 1,
deleted_at: 1,
status: 1,
created_at: -1,
},
{ partialFilterExpression: { deleted_at: null } }
);
Comment.index({
asset_id: 1,
created_at: -1,
});
Comment.index({
asset_id: 1,
created_at: 1,
});
Comment.index({
author_id: 1,
created_at: -1,
});
Comment.index({
asset_id: 1,
status: 1,
});
Comment.index({
asset_id: 1,
parent_id: 1,
reply_count: -1,
created_at: -1,
});
Comment.index({
asset_id: 1,
reply_count: -1,
created_at: -1,
});
Comment.index(
{
'action_counts.flag': 1,
status: 1,
created_at: -1,
},
{
background: true,
partialFilterExpression: {
'action_counts.flag': { $exists: true, $gt: 0 },
deleted_at: null,
},
}
);
@@ -210,34 +185,10 @@ Comment.index(
status: 1,
},
{
background: true,
}
);
// Optimize for tag searches/counts.
Comment.index(
{
'tags.tag.name': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for sorting based on the created_at timestamp
// but also good at locating comments that have a specific asset id.
Comment.index(
{
asset_id: 1,
created_at: 1,
},
{
background: true,
}
);
Comment.virtual('edited').get(function() {
return this.body_history.length > 1;
});
+2
View File
@@ -12,6 +12,8 @@ const Setting = new Schema(
id: {
type: String,
default: '1',
unique: 1,
index: true,
},
moderation: {
type: String,
+19 -28
View File
@@ -58,6 +58,7 @@ const User = new Schema(
default: uuid.v4,
unique: true,
required: true,
index: true,
},
// This is sourced from the social provider or set manually during user setup
@@ -107,6 +108,7 @@ const User = new Schema(
status: {
type: String,
enum: USER_STATUS_USERNAME,
index: true,
},
// History stores the history of username status changes.
@@ -135,6 +137,7 @@ const User = new Schema(
type: Boolean,
required: true,
default: false,
index: true,
},
history: [
{
@@ -226,41 +229,26 @@ User.index(
}
);
User.index(
{
lowercaseUsername: 1,
'profiles.id': 1,
created_at: -1,
},
{
background: true,
}
);
User.index({
lowercaseUsername: 1,
'profiles.id': 1,
created_at: -1,
});
// This query is executed often, to count the number of flagged accounts with
// usernames.
User.index(
{
'action_counts.flag': 1,
'status.username.status': 1,
},
{
background: true,
}
);
User.index({
'action_counts.flag': 1,
'status.username.status': 1,
});
// Sorting users by created at is the default people search.
User.index(
{
created_at: -1,
},
{
background: true,
}
);
User.index({
created_at: -1,
});
/**
* returns true if a commenter is staff
* returns true if a commenter is staff.
*/
User.method('isStaff', function() {
return this.role !== 'COMMENTER';
@@ -330,6 +318,9 @@ User.virtual('hasVerifiedEmail').get(function() {
});
});
/**
* system returns true when the user is a system user.
*/
User.virtual('system')
.get(function() {
return this._system;
-27
View File
@@ -1,27 +0,0 @@
{
"name": "talk",
"version": "4.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
},
"react-side-effect": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.5.tgz",
"integrity": "sha512-Z2ZJE4p/jIfvUpiUMRydEVpQRf2f8GMHczT6qLcARmX7QRb28JDBTpnM2g/i5y/p7ZDEXYGHWg0RbhikE+hJRw==",
"requires": {
"exenv": "1.2.2",
"shallowequal": "1.0.2"
}
},
"shallowequal": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz",
"integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw=="
}
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "4.4.0",
"version": "4.4.1",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
@@ -219,6 +219,7 @@
"babel-plugin-dynamic-import-node": "^1.1.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"browserstack-local": "^1.3.0",
"casual": "^1.5.19",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-datetime": "^1.5.0",
+15 -3
View File
@@ -11,10 +11,22 @@ function getReactionConfig(reaction) {
if (CREATE_MONGO_INDEXES) {
// Create the index on the comment model based on the reaction config.
Comment.collection.createIndex(
Comment.collection.ensureIndex(
{
created_at: 1,
[`action_counts.${sc(reaction)}`]: 1,
asset_id: 1,
[`action_counts.${sc(reaction)}`]: -1,
created_at: -1,
},
{
background: true,
}
);
Comment.collection.ensureIndex(
{
asset_id: 1,
[`action_counts.${sc(reaction)}`]: -1,
created_at: -1,
},
{
background: true,
+1 -1
View File
@@ -71,7 +71,7 @@ module.exports = {
permalink: asset.url,
comment_type: 'comment',
comment_content: input.body,
is_test: true,
is_test: false,
});
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
@@ -0,0 +1,9 @@
import * as actions from './constants';
export const startAttach = () => ({
type: actions.START_ATTACH,
});
export const finishAttach = () => ({
type: actions.FINISH_ATTACH,
});
@@ -45,6 +45,10 @@ class AddEmailAddressDialog extends React.Component {
),
};
componentDidMount() {
this.props.startAttach();
}
onChange = e => {
const { name, value } = e.target;
this.setState(
@@ -99,7 +103,13 @@ class AddEmailAddressDialog extends React.Component {
});
};
confirmChanges = async () => {
finish = () => {
this.props.finishAttach();
};
confirmChanges = async e => {
e.preventDefault();
if (!this.validate()) {
this.showErrors();
return;
@@ -113,7 +123,11 @@ class AddEmailAddressDialog extends React.Component {
email: emailAddress,
password: confirmPassword,
});
this.props.notify('success', 'Email Added!');
this.props.notify(
'success',
t('talk-plugin-local-auth.add_email.added.alert')
);
this.goToNextStep();
} catch (err) {
this.props.notify('error', getErrorMessages(err));
@@ -143,13 +157,13 @@ class AddEmailAddressDialog extends React.Component {
)}
{step === 1 &&
!settings.requireEmailConfirmation && (
<EmailAddressAdded done={() => {}} />
<EmailAddressAdded done={this.finish} />
)}
{step === 1 &&
settings.requireEmailConfirmation && (
<VerifyEmailAddress
emailAddress={formData.emailAddress}
done={() => {}}
done={this.finish}
/>
)}
</Dialog>
@@ -161,6 +175,8 @@ AddEmailAddressDialog.propTypes = {
attachLocalAuth: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
startAttach: PropTypes.func.isRequired,
finishAttach: PropTypes.func.isRequired,
};
export default AddEmailAddressDialog;
@@ -41,7 +41,7 @@ const AddEmailContent = ({
</li>
</ul>
<form autoComplete="off">
<form autoComplete="off" onSubmit={confirmChanges}>
<InputField
id="emailAddress"
label={t('talk-plugin-local-auth.add_email.enter_email_address')}
@@ -82,12 +82,9 @@ const AddEmailContent = ({
showSuccess={false}
/>
<div className={styles.actions}>
<a
className={cn(styles.button, styles.proceed)}
onClick={confirmChanges}
>
<button className={cn(styles.button, styles.proceed)}>
{t('talk-plugin-local-auth.add_email.add_email_address')}
</a>
</button>
</div>
</form>
</div>
@@ -4,10 +4,74 @@ 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';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
const initialState = {
showError: false,
formData: {
confirmPassword: '',
},
errors: {},
};
class ChangeEmailContentDialog extends React.Component {
state = {
showError: false,
state = initialState;
clearForm = () => {
this.setState(initialState);
};
addError = err => {
this.setState(({ errors }) => ({
errors: { ...errors, ...err },
}));
};
removeError = errKey => {
this.setState(state => {
const { [errKey]: _, ...errors } = state.errors;
return {
errors,
};
});
};
fieldValidation = (value, type, name) => {
if (!value.length) {
this.addError({
[name]: t('talk-plugin-local-auth.change_password.required_field'),
});
} else if (!validate[type](value)) {
this.addError({ [name]: errorMsj[type] });
} else {
this.removeError(name);
}
};
onChange = e => {
const { name, value, type, dataset } = e.target;
const validationType = dataset.validationType || type;
this.setState(
state => ({
formData: {
...state.formData,
[name]: value,
},
}),
() => {
this.fieldValidation(value, validationType, name);
}
);
};
hasError = err => {
return Object.keys(this.state.errors).indexOf(err) !== -1;
};
getError = errorKey => {
return this.state.errors[errorKey];
};
showError = () => {
@@ -16,24 +80,31 @@ class ChangeEmailContentDialog extends React.Component {
});
};
cancel = () => {
this.clearForm();
this.props.closeDialog();
};
confirmChanges = async e => {
e.preventDefault();
const { confirmPassword = '' } = this.state.formData;
if (this.formHasError()) {
this.showError();
return;
}
await this.props.save();
await this.props.save(confirmPassword);
this.props.next();
};
formHasError = () => this.props.hasError('confirmPassword');
formHasError = () => this.hasError('confirmPassword');
render() {
return (
<div>
<span className={styles.close} onClick={this.props.cancel}>
<span className={styles.close} onClick={this.cancel}>
×
</span>
<h1 className={styles.title}>
@@ -59,17 +130,17 @@ class ChangeEmailContentDialog extends React.Component {
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')}
onChange={this.onChange}
value={this.state.formData.confirmPassword}
hasError={this.hasError('confirmPassword')}
errorMsg={this.getError('confirmPassword')}
showError={this.state.showError}
columnDisplay
/>
<div className={styles.bottomActions}>
<Button
className={styles.cancel}
onClick={this.props.cancel}
onClick={this.cancel}
type="button"
>
{t('talk-plugin-local-auth.change_email.cancel')}
@@ -86,14 +157,11 @@ class ChangeEmailContentDialog extends React.Component {
}
ChangeEmailContentDialog.propTypes = {
save: PropTypes.func,
next: PropTypes.func,
cancel: PropTypes.func,
onChange: PropTypes.func,
save: PropTypes.func,
formData: PropTypes.object,
email: PropTypes.string,
hasError: PropTypes.func,
getError: PropTypes.func,
closeDialog: PropTypes.func,
};
export default ChangeEmailContentDialog;
@@ -114,11 +114,11 @@ class Profile extends React.Component {
isSaveEnabled = () => {
const { formData } = this.state;
const { emailAddress, username } = this.props;
const { root: { me: { username, email } } } = this.props;
const formHasErrors = !!Object.keys(this.state.errors).length;
const validUsername =
formData.newUsername && formData.newUsername !== username;
const validEmail = formData.newEmail && formData.newEmail !== emailAddress;
const validEmail = formData.newEmail && formData.newEmail !== email;
return !formHasErrors && (validUsername || validEmail);
};
@@ -138,8 +138,8 @@ class Profile extends React.Component {
}
};
saveEmail = async () => {
const { newEmail, confirmPassword } = this.state.formData;
saveEmail = async confirmPassword => {
const { newEmail } = this.state.formData;
try {
await this.props.updateEmailAddress({
@@ -202,12 +202,10 @@ class Profile extends React.Component {
)}
<ChangeEmailContentDialog
save={this.saveEmail}
onChange={this.onChange}
formData={this.state.formData}
email={email}
enable={formData.newEmail && email !== formData.newEmail}
hasError={this.hasError}
getError={this.getError}
closeDialog={this.closeDialog}
/>
</ConfirmChangesDialog>
@@ -0,0 +1,4 @@
const prefix = 'TALK_LOCAL_AUTH';
export const START_ATTACH = `${prefix}_START_ATTACH`;
export const FINISH_ATTACH = `${prefix}_FINISH_ATTACH`;
@@ -3,10 +3,16 @@ import { bindActionCreators } from 'redux';
import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import AddEmailAddressDialog from '../components/AddEmailAddressDialog';
import { notify } from 'coral-framework/actions/notification';
import { withAttachLocalAuth } from '../hocs';
import { startAttach, finishAttach } from '../actions';
import get from 'lodash/get';
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
const mapStateToProps = ({ talkPluginLocalAuth: state }) => ({
inProgress: state.inProgress,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ notify, startAttach, finishAttach }, dispatch);
const withData = withFragments({
root: gql`
@@ -14,6 +20,13 @@ const withData = withFragments({
me {
id
email
state {
status {
username {
status
}
}
}
}
settings {
requireEmailConfirmation
@@ -23,8 +36,13 @@ const withData = withFragments({
});
export default compose(
connect(null, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps),
withAttachLocalAuth,
withData,
excludeIf(({ root: { me } }) => !me || me.email)
excludeIf(
({ root: { me }, inProgress }) =>
!me ||
get(me, 'state.status.username.status') === 'UNSET' ||
(me.email && !inProgress)
)
)(AddEmailAddressDialog);
@@ -1,10 +1,12 @@
import ChangePassword from './containers/ChangePassword';
import AddEmailAddressDialog from './containers/AddEmailAddressDialog';
import Profile from './containers/Profile';
import translations from './translations.yml';
import translations from '../translations.yml';
import graphql from './graphql';
import reducer from './reducer';
export default {
reducer,
translations,
slots: {
profileHeader: [Profile],
@@ -0,0 +1,20 @@
import * as actions from './constants';
const initialState = {
inProgress: false,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case actions.START_ATTACH:
return {
inProgress: true,
};
case actions.FINISH_ATTACH:
return {
inProgress: false,
};
default:
return state;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ const mutators = require('./server/mutators');
const path = require('path');
module.exports = {
translations: path.join(__dirname, 'server', 'translations.yml'),
translations: path.join(__dirname, 'translations.yml'),
typeDefs,
mutators,
resolvers,
@@ -1,9 +0,0 @@
en:
email:
email_change_original:
subject: Email change
body: Your email address has been changed from {0} to {1}. If you did not initiate this change, please contact {2}. # TODO: update translation
error:
NO_LOCAL_PROFILE: No existing email address is associated with this account.
LOCAL_PROFILE: An email address is already associated with this account.
INCORRECT_PASSWORD: Provided password was incorrect.
@@ -1,4 +1,12 @@
en:
email:
email_change_original:
subject: Email change
body: Your email address has been changed from {0} to {1}. If you did not request this change, please contact {2}.
error:
NO_LOCAL_PROFILE: No existing email address is associated with this account.
LOCAL_PROFILE: An email address is already associated with this account.
INCORRECT_PASSWORD: Provided password was incorrect.
talk-plugin-local-auth:
change_password:
change_password: "Change Password"
@@ -43,7 +51,7 @@ en:
email_does_not_match: "Email Address does not match"
insert_password: "Insert Password:"
required_field: "This field is required"
done: "done"
done: "Done"
content:
title: "Add an Email Address"
description: "For your added security, we require users to add an email address to their accounts. Your email address will be used to:"
@@ -59,6 +67,7 @@ en:
subtitle: "Need to change your email address?"
description_2: "You can change your account settings by visiting"
path: "My Profile > Settings"
alert: "Email Added!"
es:
talk-plugin-local-auth:
change_password:
@@ -1,12 +1,19 @@
@custom-media --small-viewport (min-width: 425px);
.dialog {
width: calc(100% - 50px);
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: 380px;
top: 10px;
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
font-size: 14px;
border-radius: 4px;
padding: 20px;
@media (--small-viewport) {
width: 380px;
}
}
.close {
@@ -7,7 +7,7 @@ en:
date: "When you wrote the comment"
url: "The permalink URL for the comment"
body: "The comment text"
asset_url: "The URL on the article or story where the comment appears"
asset_url: "The URL of the article or story where the comment appears"
confirm: "Download My Comment History"
email:
download:
@@ -17,7 +17,7 @@ en:
delete:
subject: "Your account for {0} is scheduled to be deleted"
body: |
A request to delete your account was received. Your account is scheduled for deletion on {1}.
A request to delete your account was received. Your account is scheduled for deletion on {1}.
After that time all of your comments will be removed from the site, all of your comments will be removed from our database, and your username and email address will be removed from our system.
@@ -1,5 +1,12 @@
@custom-media --narrow-viewport (max-width: 420px);
.commentContent {
composes: content from "./CommentContent.css";
/* Prevent zoom on narrow viewports */
@media (--narrow-viewport) {
font-size: 16px;
}
}
.placeholder {
@@ -8,6 +8,7 @@ import RTE from './rte/RTE';
import { Icon } from 'plugin-api/beta/client/components/ui';
import { Bold, Italic, Blockquote } from './rte/features';
import { t } from 'plugin-api/beta/client/services';
import bowser from 'bowser';
class Editor extends React.Component {
ref = null;
@@ -40,7 +41,9 @@ class Editor extends React.Component {
}
});
}
if (this.props.isReply) {
// Skip IOS due to a bug, see https://www.pivotaltracker.com/story/show/157434928
if (this.props.isReply && !bowser.ios) {
this.ref.focus();
}
}
+9
View File
@@ -0,0 +1,9 @@
.container {
width: auto;
max-width: 680px;
padding: 0 15px;
}
.graphiql em {
font-family: georgia;
}
+27 -18
View File
@@ -1,49 +1,58 @@
const express = require('express');
const router = express.Router();
const errors = require('../../errors');
const Assets = require('../../services/assets');
const body =
'Lorem ipsum dolor sponge amet, consectetur adipiscing clam. Ut lobortis sollicitudin pillar a ornare. Curabitur dignissim vestibulum cay non rhoncus. Cras laoreet ante vel nunc hendrerit, shelf imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Talk volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non trust nec neque congue faucibus porttitor sit amet elkhorn.';
const casual = require('casual');
const { ErrNotFound } = require('../../errors');
const Asset = require('../../models/asset');
router.get('/id/:asset_id', async (req, res, next) => {
try {
const asset = await Assets.findById(req.params.asset_id);
const asset = await Asset.findOne({ id: req.params.asset_id });
if (asset === null) {
return next(errors.ErrNotFound);
throw new ErrNotFound();
}
res.render('dev/article', {
title: asset.title,
asset_id: asset.id,
asset_url: asset.url,
body: '',
basePath: '/client/embed/stream',
});
} catch (err) {
return next(err);
}
});
router.get('/random', (req, res) => {
const title = casual.title;
res.redirect(`./title/${title.replace(/ /g, '-')}`);
});
router.get('/title/:asset_title', (req, res) => {
return res.render('dev/article', {
res.render('dev/article', {
title: req.params.asset_title.split('-').join(' '),
asset_url: '',
asset_id: null,
body: body,
basePath: '/client/embed/stream',
});
});
router.get('/', async (req, res, next) => {
let skip = req.query.skip ? parseInt(req.query.skip) : 0;
let limit = req.query.limit ? parseInt(req.query.limit) : 25;
try {
const assets = await Assets.all(skip, limit);
const skip = req.query.skip ? parseInt(req.query.skip) : 0;
const limit = req.query.limit ? parseInt(req.query.limit) : 6;
const [assets, count] = await Promise.all([
Asset.find({})
.sort({ created_at: 1 })
.limit(limit)
.skip(skip),
Asset.count(),
]);
res.render('dev/articles', {
assets: assets,
skip,
limit,
count,
assets,
});
} catch (err) {
return next(err);
-2
View File
@@ -16,8 +16,6 @@ router.get('/', staticTemplate, async (req, res) => {
title: 'Coral Talk',
asset_url: '',
asset_id: '',
body: '',
basePath: '/static/embed/stream',
});
}
});
-6
View File
@@ -232,11 +232,5 @@ module.exports = class AssetsService {
await AssetModel.remove({
id: srcAssetID,
});
// That's it!
}
static all(limit = undefined) {
return AssetModel.find({}).limit(limit);
}
};
+7 -3
View File
@@ -1,13 +1,17 @@
const Setting = require('../models/setting');
const { ErrSettingsNotInit } = require('../errors');
const { dotize } = require('./utils');
const { isEmpty, zipObject, uniq } = require('lodash');
const { isEmpty, zipObject } = require('lodash');
const DataLoader = require('dataloader');
const selector = { id: '1' };
async function loadFn(fields = []) {
const model = await Setting.findOne(selector).select(uniq(fields));
async function loadFn(/* fields = [] */) {
// Originally, we used the projection operation, turns out this isn't that
// fast. We should utilize the redis cache instead here.
// const model = await Setting.findOne(selector).select(uniq(fields));
const model = await Setting.findOne(selector);
if (!model) {
throw new ErrSettingsNotInit();
}
+2 -7
View File
@@ -3,16 +3,11 @@
<head>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<title>Talk - Coral Admin</title>
<style media="screen">
html, body, #root, #root > div { min-height: 100%; }
body { margin: 0; background-color: #FAFAFA; font-family: 'Roboto', sans-serif; }
</style>
<%- include partials/head %>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<%- include partials/head %>
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
<%- include partials/custom-css %>
</head>
<body class="admin-page">
<div id="root"></div>
+9 -3
View File
@@ -5,13 +5,17 @@
<meta charset="utf-8" />
<title>GraphiQL</title>
<meta name="robots" content="noindex" />
<%- include ../partials/dev %>
<style>
html, body {
html, body, #graphiql {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
*, ::after, ::before {
box-sizing: inherit;
}
</style>
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
@@ -20,6 +24,8 @@
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
</head>
<body>
<%- include ../partials/dev-nav %>
<main id="graphiql"></main>
<script>
// Collect the URL parameters
var parameters = {};
@@ -112,8 +118,8 @@
variables: null,
operationName: null,
}),
document.body
document.querySelector("#graphiql")
);
</script>
</body>
</html>
</html>
+36 -44
View File
@@ -8,53 +8,45 @@
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
<meta property="article:section" itemprop="articleSection" content="The Section!" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
main {
margin-left:auto;
margin-right:auto;
max-width:500px;
display: block;
}
</style>
<title><%= title %></title>
<%- include ../partials/dev %>
</head>
<body>
<main>
<h1><%= title %></h1>
<p><%= body %></p>
<p><a href="<%= BASE_PATH %>admin">Admin</a> - <a href="<%= BASE_PATH %>dev/assets">All Assets</a></p>
<div id='coralStreamEmbed'></div>
<script src="<%= resolve('embed.js') %>" async onload="
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
talk: '<%= BASE_URL %>',
asset_url: '<%= asset_url ? asset_url : '' %>',
asset_id: '<%= asset_id ? asset_id : '' %>',
auth_token: '',
/**
* You can listen to events using the example below.
* The argument passed is the event emitter from
* https://github.com/asyncly/EventEmitter2
*
* events: function(events) {
* events.onAny(function(eventName, data) {
* console.log(eventName, data);
* });
* },
*/
plugins_config: {
<%- include ../partials/dev-nav %>
<div class="container">
<h1 class="mt-3"><%= title %></h1>
<div id='coralStreamEmbed'></div>
<script src="<%= resolve('embed.js') %>"></script>
<script>
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
talk: '<%= BASE_URL %>',
asset_url: '<%= asset_url ? asset_url : '' %>',
asset_id: '<%= asset_id ? asset_id : '' %>',
auth_token: '',
/**
* You can disable rendering slot components of a plugin by doing:
*
* 'talk-plugin-love': {
* disable_components: true,
* },
*/
test: 'data',
debug: false
}
})
"></script>
</main>
* You can listen to events using the example below.
* The argument passed is the event emitter from
* https://github.com/asyncly/EventEmitter2
*
* events: function(events) {
* events.onAny(function(eventName, data) {
* console.log(eventName, data);
* });
* },
*/
plugins_config: {
/**
* You can disable rendering slot components of a plugin by doing:
*
* 'talk-plugin-love': {
* disable_components: true,
* },
*/
test: 'data',
debug: false
}
})
</script>
</div>
</body>
</html>
+38 -12
View File
@@ -1,14 +1,40 @@
<html>
<body>
<h1>
Asset list
</h1>
<% assets.forEach(function (asset) { %>
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>"><%= asset.url %></a><br />
<% }) %>
<p>
(For dev use only. FYI, you can: ?skip=100&limit=25)
</p>
</body>
<head>
<title>All Assets</title>
<%- include ../partials/dev %>
</head>
<body>
<%- include ../partials/dev-nav %>
<div class="container">
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
<h1 class="mb-0">All Assets</h1>
<span class="text-muted"><%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets</span>
</div>
<div class="list-group">
<% if (skip === 0) { %><a href="<%= BASE_PATH %>dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a><% } %>
<% assets.forEach(function (asset) { %>
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1"><%= asset.title %></h5>
<small>Created <%= asset.created_at.toLocaleString('en-US') %></small>
</div>
<small><%= asset.url %></small>
</a>
<% }) %>
</div>
<% if (count !== assets.length) { %>
<nav aria-label="Page navigation example" class="mt-2">
<ul class="pagination justify-content-center">
<% let page = 1; for (let i = 0; i < count; i += limit) { %>
<% if (i === skip) { %>
<li class="page-item disabled"><a class="page-link" href="#"><%= page %></a></li>
<% } else { %>
<li class="page-item"><a class="page-link" href="?skip=<%= i %>&amp;limit=<%= limit %>"><%= page %></a></li>
<% } %>
<% page++; } %>
</ul>
</nav>
<% } %>
</div>
</body>
</html>
+1
View File
@@ -5,6 +5,7 @@
<%- include ../partials/head %>
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/default.css') %>">
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/bundle.css') %>">
<%- include ../partials/custom-css %>
</head>
<body class="embed-stream-page">
<div id="talk-embed-stream-container"></div>
+1
View File
@@ -6,6 +6,7 @@
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<%- include partials/head %>
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
<%- include partials/custom-css %>
</head>
<body>
<div id="talk-login-container"></div>
+2 -1
View File
@@ -1,3 +1,4 @@
<%- include ./head %>
<%- include head %>
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
<%- include custom-css %>
+1
View File
@@ -0,0 +1 @@
<% if (locals.customCssUrl) { %><link href="<%= customCssUrl %>" rel="stylesheet" type="text/css"><% } %>
+8
View File
@@ -0,0 +1,8 @@
<nav class="navbar navbar-dark bg-dark">
<a class="navbar-brand" href="<%= BASE_PATH %>dev"><%= organizationName %> <span class="text-muted">Organization</span></a>
<form class="form-inline mb-0">
<a href="<%= BASE_PATH %>admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
<a href="<%= BASE_PATH %>api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
<a href="<%= BASE_PATH %>dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
</form>
</nav>
+5
View File
@@ -0,0 +1,5 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous" rel="stylesheet">
<link href="<%= BASE_PATH %>public/css/dev.css" rel="stylesheet" >
-3
View File
@@ -18,9 +18,6 @@
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
<%_ if (locals.customCssUrl) { _%>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<%_ } _%>
<%- include data %>
<base href="<%= BASE_URL %>"/>
+15
View File
@@ -1828,6 +1828,13 @@ caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
casual@^1.5.19:
version "1.5.19"
resolved "https://registry.yarnpkg.com/casual/-/casual-1.5.19.tgz#66fac46f7ae463f468f5913eb139f9c41c58bbf2"
dependencies:
mersenne-twister "^1.0.1"
moment "^2.15.2"
center-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
@@ -7154,6 +7161,10 @@ merge@^1.1.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
mersenne-twister@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
metascraper-author@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-3.9.2.tgz#ff2020ac428f59a875d655df3b0d4bea171fde19"
@@ -7436,6 +7447,10 @@ moment@^2.10.3:
version "2.19.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
moment@^2.15.2:
version "2.22.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
mongodb-core@2.1.17:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.17.tgz#a418b337a14a14990fb510b923dee6a813173df8"