Merge branch 'master' into client-targets

This commit is contained in:
Wyatt Johnson
2017-06-08 17:32:51 -06:00
committed by GitHub
127 changed files with 2251 additions and 766 deletions
+1
View File
@@ -10,4 +10,5 @@ plugins/*
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
!plugins/coral-plugin-viewing-options
node_modules
+1
View File
@@ -23,5 +23,6 @@ plugins/*
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
!plugins/coral-plugin-viewing-options
**/node_modules/*
+24
View File
@@ -342,6 +342,30 @@ module.exports = {
}
```
#### Field: `tags`
The tags hook allows a plugin to define tags that are code controlled (added
or enabled by code). Below is an example pulled from the core off topic plugin
on how to create a hook for the `OFF_TOPIC` name:
```js
[
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
```
You can refer to `models/schema/tag.js` for the available schema to match when
creating models to enable/disable specific features.
#### Field: `passport`
```js
+1
View File
@@ -13,6 +13,7 @@ program
.command('setup', 'setup the application')
.command('jobs', 'work with the job queues')
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('plugins', 'provides utilities for interacting with the plugin system')
.parse(process.argv);
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const MigrationService = require('../services/migration');
// Register shutdown hooks.
util.onshutdown([
() => mongoose.disconnect()
]);
async function createMigration(name) {
try {
// Create the migration.
await MigrationService.create(name);
util.shutdown();
} catch (e) {
console.error(e);
util.shutdown(1);
}
}
async function runMigrations() {
try {
let {backedUp} = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false
}
]);
if (!backedUp) {
throw new Error('Please backup your databases prior to migrations occuring');
}
// Get the migrations to run.
let migrations = await MigrationService.listPending();
console.log('Now going to run the following migrations:\n');
for (let {filename} of migrations) {
console.log(`\tmigrations/${filename}`);
}
let {confirm} = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false
}
]);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations);
} else {
console.warn('Skipping migrations');
}
util.shutdown();
} catch (e) {
console.error(e);
util.shutdown(1);
}
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('create <name>')
.description('creates a new migration')
.action(createMigration);
program
.command('run')
.description('runs all pending migrations')
.action(runMigrations);
program.parse(process.argv);
// If there is no command listed, output help.
if (process.argv.length <= 2) {
program.outputHelp();
util.shutdown();
}
+44 -1
View File
@@ -2,9 +2,13 @@
const program = require('./commander');
const app = require('../app');
const debug = require('debug')('talk:cli:serve');
const errors = require('../errors');
const {createServer} = require('http');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const MigrationService = require('../services/migration');
const SetupService = require('../services/setup');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('./util');
@@ -87,7 +91,46 @@ function onListening() {
/**
* Start the app.
*/
function startApp(program) {
async function startApp(program) {
try {
// Check to see if the application is installed. If the application
// has been installed, then it will throw errors.ErrSettingsNotInit, this
// just means we don't have to check that the migrations have run.
await SetupService.isAvailable();
debug('setup is currently available, migrations not being checked');
} catch (e) {
// Check the error.
switch (e) {
case errors.ErrInstallLock, errors.ErrSettingsInit:
debug('setup is not currently available, migrations now being checked');
// The error was expected, just continue.
break;
default:
// The error was not expected, throw the error!
throw e;
}
// Now try and check the migration status.
try {
// Verify that the minimum migration version is met.
await MigrationService.verify();
} catch (e) {
console.error(e);
process.exit(1);
}
debug('migrations do not have to be run');
}
/**
* Listen on provided port, on all network interfaces.
+139 -149
View File
@@ -8,6 +8,7 @@ const program = require('./commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingModel = require('../models/setting');
const MODERATION_OPTIONS = require('../models/enum/moderation_options');
const SettingsService = require('../services/settings');
const SetupService = require('../services/setup');
const UsersService = require('../services/users');
@@ -32,166 +33,155 @@ program
// Setup the application
//==============================================================================
const performSetup = () => {
if (program.defaults) {
return SettingsService
.init()
.then(() => {
console.log('Settings created.');
console.log('\nTalk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
const performSetup = async () => {
// Get the current settings, we are expecing an error here.
return SettingsService
.retrieve()
.then(() => {
try {
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
throw errors.ErrSettingsInit;
// Try to get the settings.
await SettingsService.retrieve();
})
.catch((err) => {
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
throw errors.ErrSettingsInit;
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (err !== errors.ErrSettingsNotInit) {
throw err;
} catch (e) {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (e !== errors.ErrSettingsNotInit) {
throw e;
}
}
if (program.defaults) {
await SettingsService.init();
console.log('Settings created.');
console.log('\nTalk is now installed!');
return;
}
// Create the base settings model.
let settings = new SettingModel();
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
let answers = await inquirer.prompt([
{
type: 'input',
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
{
type: 'list',
choices: MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
}
]);
})
.then(() => {
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
});
// Create the base settings model.
let settings = new SettingModel();
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
return inquirer.prompt([
{
type: 'input',
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
{
type: 'list',
choices: SettingModel.MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
}
])
.then((answers) => {
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
});
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
return inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username, false)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
if (value && value.length >= 3) {
return true;
}
return 'Email is required';
}
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
]);
})
.then((user) => {
if (user.password !== user.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
let user = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username, false)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
if (value && value.length >= 3) {
return true;
}
return SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
username: user.username,
password: user.password
}
});
});
})
.then(({user}) => {
console.log('Settings created.');
console.log(`User ${user.id} created.`);
console.log('\nTalk is now installed!');
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
return 'Email is required';
}
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
]);
if (user.password !== user.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
let {user: newUser} = await SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
username: user.username,
password: user.password
}
});
console.log('Settings created.');
console.log(`User ${newUser.id} created.`);
console.log('\nTalk is now installed!');
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
};
// Start tthe setup process.
performSetup();
performSetup()
.then(() => {
util.shutdown();
})
.catch((e) => {
console.error(e);
util.shutdown(1);
});
+5 -6
View File
@@ -4,21 +4,20 @@
"es6": true,
"mocha": true
},
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -26,8 +26,7 @@ const fm = new IntrospectionFragmentMatcher({
{name: 'SetUserStatusResponse'},
{name: 'SuspendUserResponse'},
{name: 'SetCommentStatusResponse'},
{name: 'AddCommentTagResponse'},
{name: 'RemoveCommentTagResponse'},
{name: 'ModifyTagResponse'},
{name: 'IgnoreUserResponse'},
{name: 'StopIgnoringUserResponse'}
]
@@ -37,3 +37,13 @@ export const viewAllComments = () => {
return {type: actions.VIEW_ALL_COMMENTS};
};
export const addCommentClassName = (className) => ({
type: actions.ADD_COMMENT_CLASSNAME,
className
});
export const removeCommentClassName = (idx) => ({
type: actions.REMOVE_COMMENT_CLASSNAME,
idx
});
@@ -98,6 +98,10 @@
text-align: right;
}
.commentInfoBar {
float: right;
}
@keyframes enter {
0% {background-color: rgba(0, 0, 0, 0);}
50% {background-color: rgba(255,255,0, 0.2);}
@@ -1,8 +1,7 @@
import React, {PropTypes} from 'react';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import TagLabel from 'coral-plugin-tag-label/TagLabel';
import Content from 'coral-plugin-commentcontent/CommentContent';
import PubDate from 'coral-plugin-pubdate/PubDate';
@@ -20,14 +19,15 @@ import {
} from 'coral-plugin-best/BestButton';
import Slot from 'coral-framework/components/Slot';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {EditableCommentContent} from './EditableCommentContent';
import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils';
import {getEditableUntilDate} from './util';
import styles from './Comment.css';
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF');
const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF');
const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length;
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second newest comment in
@@ -73,8 +73,7 @@ const ActionButton = ({children}) => {
);
};
class Comment extends React.Component {
export default class Comment extends React.Component {
constructor(props) {
super(props);
@@ -181,10 +180,10 @@ class Comment extends React.Component {
commentIsIgnored: React.PropTypes.func,
// dispatch action to add a tag to a comment
addCommentTag: React.PropTypes.func,
addTag: React.PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: React.PropTypes.func,
removeTag: React.PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
@@ -272,28 +271,29 @@ class Comment extends React.Component {
}
render () {
const {
comment,
parentId,
currentUser,
asset,
depth,
postComment,
addNotification,
showSignInDialog,
highlighted,
comment,
postFlag,
parentId,
ignoreUser,
highlighted,
postComment,
currentUser,
postDontAgree,
setActiveReplyBox,
activeReplyBox,
deleteAction,
addCommentTag,
removeCommentTag,
ignoreUser,
liveUpdates,
disableReply,
commentIsIgnored,
maxCharCount,
charCountEnable
addNotification,
charCountEnable,
showSignInDialog,
addTag,
removeTag,
liveUpdates,
commentIsIgnored,
commentClassNames = []
} = this.props;
const view = this.getVisibileReplies();
@@ -333,25 +333,57 @@ class Comment extends React.Component {
const addBestTag = notifyOnError(
() =>
addCommentTag({
addTag({
id: comment.id,
tag: BEST_TAG
name: BEST_TAG,
assetId: asset.id
}),
() => 'Failed to tag comment as best'
);
const removeBestTag = notifyOnError(
() =>
removeCommentTag({
removeTag({
id: comment.id,
tag: BEST_TAG
name: BEST_TAG,
assetId: asset.id
}),
() => 'Failed to remove best comment tag'
);
/**
* classNamesToAdd
* adds classNames based on condition
* classnames is an array of objects with key as classnames and value as conditions
* i.e:
* {
* 'myClassName': { tags: ['STAFF']}
* }
*
* This will add myClassName to comments tagged with STAFF TAG.
* **/
const classNamesToAdd = commentClassNames.reduce((acc, className) => {
let res = [];
// Adding classNames based on tags
Object.keys(className).forEach((cn) => {
const condition = className[cn];
condition.tags.forEach((tag) => {
if (hasTag(comment.tags, tag)) {
res = [...res, cn];
}
});
});
// TODO: Compare rest of the comment obj to the condition if needed
return [...acc, ...res];
}, []);
return (
<div
className={cn(commentClass, 'talk-stream-comment-wrapper', {[styles.enter]: this.state.animateEnter})}
className={cn(commentClass, 'talk-stream-comment-wrapper', classNamesToAdd, {[styles.enter]: this.state.animateEnter})}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}
>
@@ -376,11 +408,13 @@ class Comment extends React.Component {
</span>
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
data={this.props.data}
root={this.props.root}
depth={depth}
comment={comment}
commentId={comment.id}
data={this.props.data}
root={this.props.root}
inline
/>
@@ -515,8 +549,8 @@ class Comment extends React.Component {
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
addTag={addTag}
removeTag={removeTag}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
@@ -542,8 +576,6 @@ class Comment extends React.Component {
}
}
export default Comment;
// return whether the comment is editable
function commentIsStillEditable (comment) {
const editing = comment && comment.editing;
@@ -1,19 +1,18 @@
import React, {PropTypes} from 'react';
import LoadMore from './LoadMore';
import Comment from '../components/Comment';
import SuspendedAccount from './SuspendedAccount';
import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import Slot from 'coral-framework/components/Slot';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import {can} from 'coral-framework/services/perms';
import {ModerationLink} from 'coral-plugin-moderation';
import RestrictedMessageBox
from 'coral-framework/components/RestrictedMessageBox';
import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import t, {timeago} from 'coral-framework/services/i18n';
import {TransitionGroup} from 'react-transition-group';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
@@ -122,6 +121,7 @@ class Stream extends React.Component {
render() {
const {
commentClassNames,
root: {asset, asset: {comments}, comment, me},
postComment,
addNotification,
@@ -129,11 +129,11 @@ class Stream extends React.Component {
postDontAgree,
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag,
pluginProps,
addTag,
ignoreUser,
auth: {loggedIn, user},
removeTag,
pluginProps,
editName
} = this.props;
const view = this.getVisibleComments();
@@ -202,11 +202,17 @@ class Stream extends React.Component {
/>}
</div>
: <p>{asset.settings.closedMessage}</p>}
{loggedIn &&
{loggedIn && (
<ModerationLink
assetId={asset.id}
isAdmin={can(user, 'MODERATE_COMMENTS')}
/>}
/>
)}
<div className="talk-stream-wrapper-box">
<Slot fill="streamBox" />
</div>
{/* the highlightedComment is isolated after the user followed a permalink */}
{highlightedComment
@@ -235,7 +241,7 @@ class Stream extends React.Component {
editComment={this.props.editComment}
liveUpdates={true}
/>
: <div>
: <div className="talk-stream-comments-container">
<NewCount
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
@@ -245,6 +251,7 @@ class Stream extends React.Component {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
commentClassNames={commentClassNames}
data={this.props.data}
root={this.props.root}
disableReply={!open}
@@ -257,8 +264,8 @@ class Stream extends React.Component {
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
addTag={addTag}
removeTag={removeTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={this.props.loadNewReplies}
@@ -291,10 +298,10 @@ Stream.propTypes = {
postComment: PropTypes.func.isRequired,
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
addTag: PropTypes.func,
// dispatch action to remove a tag from a comment
removeCommentTag: PropTypes.func,
removeTag: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
@@ -41,7 +41,10 @@ class SuspendedAccount extends Component {
<span>{
canEditName ?
t('framework.edit_name.msg')
: t('framework.banned_account_msg')
:
<span>
<b>{t('framework.banned_account_header')}</b><br/> {t('framework.banned_account_body')}
</span>
}</span>
{
canEditName ?
@@ -1,3 +1,5 @@
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
@@ -28,7 +28,9 @@ export default withFragments({
created_at
status
tags {
name
tag {
name
}
}
user {
id
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
withAddTag, withRemoveTag, withIgnoreUser, withEditComment,
} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework';
@@ -290,6 +290,7 @@ const mapStateToProps = (state) => ({
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
previousTab: state.embed.previousTab,
commentClassNames: state.stream.commentClassNames
});
const mapDispatchToProps = (dispatch) =>
@@ -306,10 +307,9 @@ export default compose(
withPostComment,
withPostFlag,
withPostDontAgree,
withAddCommentTag,
withRemoveCommentTag,
withAddTag,
withRemoveTag,
withIgnoreUser,
withDeleteAction,
withEditComment,
)(StreamContainer);
+23 -6
View File
@@ -18,8 +18,8 @@ const extension = {
}
}
`,
RemoveCommentTagResponse: gql`
fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse {
RemoveTagResponse: gql`
fragment CoralEmbedStream_RemoveTagResponse on RemoveTagResponse {
comment {
id
tags {
@@ -28,8 +28,8 @@ const extension = {
}
}
`,
AddCommentTagResponse: gql`
fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse {
AddTagResponse: gql`
fragment CoralEmbedStream_AddTagResponse on AddTagResponse {
comment {
id
tags {
@@ -74,7 +74,13 @@ const extension = {
status
replyCount
tags {
name
tag {
name
created_at
}
assigned_by {
id
}
}
user {
id
@@ -144,7 +150,18 @@ const extension = {
parent_id,
asset_id,
action_summaries: [],
tags,
tags: tags.map((tag) => ({
tag: {
name: tag,
created_at: new Date().toISOString(),
__typename: 'Tag'
},
assigned_by: {
id: auth.toJS().user.id,
__typename: 'User'
},
__typename: 'TagLink'
})),
status: null,
replyCount: 0,
parent: parent_id
+21 -6
View File
@@ -19,14 +19,29 @@ loadPluginsTranslations();
injectPluginsReducers();
injectReducers(reducers);
function inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
function init(config = {}) {
store.dispatch(addExternalConfig(config));
store.dispatch(checkLogin());
}
// Don't run this in the popup.
if (!window.opener) {
pym.sendMessage('getConfig');
pym.onMessage('config', (config) => {
store.dispatch(addExternalConfig(JSON.parse(config)));
store.dispatch(checkLogin());
});
if (inIframe()) {
pym.sendMessage('getConfig');
pym.onMessage('config', (config) => {
init(JSON.parse(config));
});
} else {
init();
}
}
render(
@@ -4,6 +4,6 @@ import stream from './stream';
export default {
embed,
stream,
config,
stream,
};
@@ -1,4 +1,5 @@
import * as actions from '../constants/stream';
import * as authActions from 'coral-framework/constants/auth';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
@@ -16,29 +17,42 @@ function getQueryVariable(variable) {
const initialState = {
activeReplyBox: '',
commentCountCache: -1,
assetId: getQueryVariable('asset_id'),
assetUrl: getQueryVariable('asset_url'),
commentId: getQueryVariable('comment_id'),
commentClassNames: []
};
export default function stream(state = initialState, action) {
switch (action.type) {
case authActions.LOGOUT:
return {
...state,
activeReplyBox: '',
};
case actions.SET_ACTIVE_REPLY_BOX:
return {
...state,
activeReplyBox: action.id,
};
case actions.SET_COMMENT_COUNT_CACHE:
return {
...state,
commentCountCache: action.amount,
};
case actions.VIEW_ALL_COMMENTS:
return {
...state,
commentId: '',
};
case actions.ADD_COMMENT_CLASSNAME :
return {
...state,
commentClassNames: [...state.commentClassNames, action.className]
};
case actions.REMOVE_COMMENT_CLASSNAME :
return {
...state,
commentClassNames: [
...state.commentClassNames.slice(0, action.idx),
...state.commentClassNames.slice(action.idx + 1)
]
};
default:
return state;
}
+17 -9
View File
@@ -119,7 +119,7 @@ hr {
.coral-plugin-questionbox-info {
top: 0;
border: 0;
background: rgb(213,213,213);
background: #F0F0F0;
color: black;
width: 100%;
text-align: left;
@@ -134,26 +134,25 @@ hr {
.coral-plugin-questionbox-icon.bubble{
position: absolute;
top: 11px;
top: 8px;
left: 10px;
color: #949393;
font-size: 20px;
color: #9E9E9E;
font-size: 24px;
z-index: 0;
}
.coral-plugin-questionbox-icon.person{
z-index: 2;
top: 20px;
left: 15px;
top: 12px;
left: 12px;
position: absolute;
font-size: 24px;
color: white;
font-size: 33px;
color: #262626;
}
.coral-plugin-questionbox-box {
position: relative;
border: 0;
background: black;
color: white;
padding: 20px;
margin-left: 0px !important;
@@ -178,6 +177,15 @@ hr {
display: none;
}
.talk-stream-wrapper-box {
padding: 10px 0;
}
.talk-stream-comments-container {
position: relative;
z-index: 0;
}
.commentStream {
/* prevent absolutely positioned final permalink popover from being clipped */
padding-bottom: 50px;
@@ -1,6 +1,8 @@
.message {
background: #D8D8D8;
padding: 25px;
margin-bottom: 8px;
background-color: white;
border: 1px #F0F0F0 solid;
text-align: center;
font-weight: 100;
}
+2 -2
View File
@@ -4,9 +4,9 @@ import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
function Slot ({fill, inline = false, plugin_config: config, ...rest}) {
function Slot ({fill, inline = false, className, plugin_config: config, ...rest}) {
return (
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug})}>
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug}, className)}>
{getSlotElements(fill, {...rest, config})}
</div>
);
@@ -0,0 +1 @@
export {default as Slot} from './Slot';
+15 -19
View File
@@ -1,23 +1,19 @@
import {gql} from 'react-apollo';
import * as mutations from './mutations';
function createDefaultResponseFragments() {
const names = Object.keys(mutations).map((key) => key.replace('with', ''));
const result = {};
names.forEach((name) => {
const response = `${name}Response`;
result[response] = gql`
fragment Coral_${response} on ${response} {
errors {
translation_key
}
}
`;
});
return result;
}
import {createDefaultResponseFragments} from '../utils';
// fragments defined here are automatically registered.
export default {
...createDefaultResponseFragments()
...createDefaultResponseFragments(
'SetCommentStatusResponse',
'SuspendUserResponse',
'RejectUsernameResponse',
'SetUserStatusResponse',
'PostCommentResponse',
'EditCommentResponse',
'PostFlagResponse',
'CreateDontAgreeResponse',
'DeleteActionResponse',
'ModifyTagResponse',
'IgnoreUserResponse',
'StopIgnoringUserResponse',
)
};
+67 -14
View File
@@ -173,39 +173,93 @@ export const withDeleteAction = withMutation(
}}),
});
export const withAddCommentTag = withMutation(
const COMMENT_FRAGMENT = gql`
fragment CoralBest_UpdateFragment on Comment {
tags {
tag {
name
}
}
}
`;
export const withAddTag = withMutation(
gql`
mutation AddCommentTag($id: ID!, $tag: String!) {
addCommentTag(id:$id, tag:$tag) {
...AddCommentTagResponse
mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) {
addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
...ModifyTagResponse
}
}
`, {
props: ({mutate}) => ({
addCommentTag: ({id, tag}) => {
addTag: ({id, name, assetId}) => {
return mutate({
variables: {
id,
tag
}
name,
asset_id: assetId
},
optimisticResponse: {
addTag: {
__typename: 'ModifyTagResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `Comment_${id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
data.tags.push({
tag: {
__typename: 'Tag',
name: 'BEST'
},
__typename: 'TagLink'
});
// Write our data back to the cache.
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
},
});
}}),
});
export const withRemoveCommentTag = withMutation(
export const withRemoveTag = withMutation(
gql`
mutation RemoveCommentTag($id: ID!, $tag: String!) {
removeCommentTag(id:$id, tag:$tag) {
...RemoveCommentTagResponse
mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) {
removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) {
...ModifyTagResponse
}
}
`, {
props: ({mutate}) => ({
removeCommentTag: ({id, tag}) => {
removeTag: ({id, name, assetId}) => {
return mutate({
variables: {
id,
tag
name,
asset_id: assetId
},
optimisticResponse: {
removeTag: {
__typename: 'ModifyTagResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `Comment_${id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
const idx = data.tags.findIndex((i) => i.tag.name === 'BEST');
data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)];
// Write our data back to the cache.
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
}
});
}}),
@@ -246,4 +300,3 @@ export const withStopIgnoringUser = withMutation(
});
}}),
});
@@ -0,0 +1,16 @@
const regExp = /[-\s]+(.)?/g;
/**
* Convert dash separated strings to camel case.
*
* @param {String} str
* @return {String}
*/
export default function camelize(str) {
return str.replace(regExp, toUpper);
}
function toUpper(match, c) {
return c ? c.toUpperCase() : '';
}
+5 -4
View File
@@ -1,13 +1,14 @@
import React from 'react';
import merge from 'lodash/merge';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import uniq from 'lodash/uniq';
import pick from 'lodash/pick';
import merge from 'lodash/merge';
import plugins from 'pluginsConfig';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
import {loadTranslations} from 'coral-framework/services/i18n';
import {injectReducers} from 'coral-framework/services/store';
import camelize from './camelize';
/**
* Returns React Elements for given slot.
@@ -98,7 +99,7 @@ export function injectPluginsReducers() {
const reducers = merge(
...plugins
.filter((o) => o.module.reducer)
.map((o) => ({...o.module.reducer}))
.map((o) => ({[camelize(o.plugin)] : o.module.reducer}))
);
injectReducers(reducers);
}
+14
View File
@@ -112,3 +112,17 @@ export function getResponseErrors(mutationResult) {
});
return result.length ? result : false;
}
export function createDefaultResponseFragments(...names) {
const result = {};
names.forEach((response) => {
result[response] = gql`
fragment Coral_${response} on ${response} {
errors {
translation_key
}
}
`;
});
return result;
}
-1
View File
@@ -1 +0,0 @@
export {withReaction} from '../coral-framework/hocs';
+2 -4
View File
@@ -7,10 +7,8 @@ import classnames from 'classnames';
// tag string for best comments
export const BEST_TAG = 'BEST';
export const commentIsBest = ({tags} = {}) => {
const isBest = Array.isArray(tags) && tags.some((t) => t.name === BEST_TAG);
return isBest;
};
export const commentIsBest = ({tags} = {}) => tags.some((t) => t.tag.name === BEST_TAG);
const name = 'coral-plugin-best';
@@ -7,3 +7,7 @@ export const removeTag = (idx) => ({
type: 'REMOVE_TAG',
idx
});
export const clearTags = () => ({
type: 'CLEAR_TAGS',
});
@@ -1,2 +1,3 @@
export const ADD_TAG = 'ADD_TAG';
export const REMOVE_TAG = 'REMOVE_TAG';
export const CLEAR_TAGS = 'CLEAR_TAGS';
+3 -1
View File
@@ -1,4 +1,4 @@
import {ADD_TAG, REMOVE_TAG} from './constants';
import {ADD_TAG, REMOVE_TAG, CLEAR_TAGS} from './constants';
const initialState = {
tags: []
@@ -19,6 +19,8 @@ export default function commentBox (state = initialState, action) {
...state.tags.slice(action.idx + 1)
]
};
case CLEAR_TAGS :
return initialState;
default :
return state;
}
@@ -1,4 +1,6 @@
.moderationLink {
display: inline;
a {
color: #679af3;
text-decoration: none;
+5 -2
View File
@@ -9,6 +9,10 @@ class ReplyBox extends Component {
document.getElementById('replyText').focus();
}
cancelReply = () => {
this.props.setActiveReplyBox('');
};
render() {
const {
styles,
@@ -18,7 +22,6 @@ class ReplyBox extends Component {
addNotification,
parentId,
commentPostedHandler,
setActiveReplyBox,
maxCharCount,
charCountEnable
} = this.props;
@@ -28,7 +31,7 @@ class ReplyBox extends Component {
charCountEnable={charCountEnable}
commentPostedHandler={commentPostedHandler}
parentId={parentId}
cancelButtonClicked={setActiveReplyBox}
cancelButtonClicked={this.cancelReply}
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
+2
View File
@@ -6,6 +6,7 @@ const Assets = require('./assets');
const Comments = require('./comments');
const Metrics = require('./metrics');
const Settings = require('./settings');
const Tags = require('./tags');
const Users = require('./users');
const plugins = require('../../services/plugins');
@@ -18,6 +19,7 @@ let loaders = [
Comments,
Metrics,
Settings,
Tags,
Users,
// Load the plugin loaders from the manager.
-1
View File
@@ -1,5 +1,4 @@
const SettingsService = require('../../services/settings');
const util = require('./util');
/**
+22
View File
@@ -0,0 +1,22 @@
const DataLoader = require('dataloader');
const TagsService = require('../../services/tags');
/**
* Get all the tags for the context for the dataloader.
*/
const genAll = (context, queries) => {
return Promise.all(queries.map(({id, item_type, asset_id}) => {
return TagsService.getAll({id, item_type, asset_id});
}));
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
Tags: {
getAll: new DataLoader((queries) => genAll(context, queries))
}
});
+56 -40
View File
@@ -1,22 +1,70 @@
const debug = require('debug')('talk:graph:mutators:comment');
const errors = require('../../errors');
const ActionModel = require('../../models/action');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const TagsService = require('../../services/tags');
const CommentsService = require('../../services/comments');
const KarmaService = require('../../services/karma');
const linkify = require('linkify-it')();
const Wordlist = require('../../services/wordlist');
const {
CREATE_COMMENT,
SET_COMMENT_STATUS,
ADD_COMMENT_TAG,
REMOVE_COMMENT_TAG,
EDIT_COMMENT
} = require('../../perms/constants');
const debug = require('debug')('talk:graph:mutators:tags');
const plugins = require('../../services/plugins');
const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
debug(`added plugin '${plugin.name}'`);
acc = acc.concat(tags);
return acc;
}, []);
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
const item_type = 'COMMENTS';
// Handle Tags
if (tags.length) {
// Get the global list of tags from the dataloader.
let globalTags = await Tags.getAll.load({
item_type,
asset_id
});
if (!Array.isArray(globalTags)) {
globalTags = [];
}
globalTags = globalTags.concat(pluginTags);
// Merge in the tags for the given comment.
tags = tags.map((name) => {
// Resolve the TagLink that we can use for the comment.
let {tagLink} = TagsService.resolveLink(user, globalTags, {name, item_type});
// Return the tagLink for tag insertion.
return tagLink;
});
}
// Add the staff tag for comments created as a staff member.
if (user.can(ADD_COMMENT_TAG)) {
tags.push(TagsService.newTagLink(user, {
name: 'STAFF',
item_type
}));
}
return tags;
};
/**
* adjustKarma will adjust the affected user's karma depending on the moderators
* action.
@@ -102,15 +150,11 @@ const adjustKarma = (Comments, id, status) => async () => {
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = async ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
const {user, loaders: {Comments}, pubsub} = context;
// Building array of tags
tags = tags.map((tag) => ({name: tag}));
// If admin or moderator, adding STAFF tag
if (user.isStaff()) {
tags.push({name: 'STAFF'});
}
// Resolve the tags for the comment.
tags = await resolveTagsForComment(context, {asset_id, tags});
let comment = await CommentsService.publicCreate({
body,
@@ -305,24 +349,6 @@ const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
return comment;
};
/**
* Adds a tag to a Comment
* @param {String} id identifier of the comment (uuid)
* @param {String} tag name of the tag
*/
const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
return CommentsService.addTag(id, tag, user.id);
};
/**
* Removes a tag from a Comment
* @param {String} id identifier of the comment (uuid)
* @param {String} tag name of the tag
*/
const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
return CommentsService.removeTag(id, tag);
};
/**
* Edit a Comment
* @param {String} id identifier of the comment (uuid)
@@ -354,9 +380,7 @@ module.exports = (context) => {
Comment: {
create: () => Promise.reject(errors.ErrNotAuthorized),
setStatus: () => Promise.reject(errors.ErrNotAuthorized),
addCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
edit: () => Promise.reject(errors.ErrNotAuthorized),
edit: () => Promise.reject(errors.ErrNotAuthorized)
}
};
@@ -368,14 +392,6 @@ module.exports = (context) => {
mutators.Comment.setStatus = (action) => setStatus(context, action);
}
if (context.user && context.user.can(ADD_COMMENT_TAG)) {
mutators.Comment.addCommentTag = (action) => addCommentTag(context, action);
}
if (context.user && context.user.can(REMOVE_COMMENT_TAG)) {
mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action);
}
if (context.user && context.user.can(EDIT_COMMENT)) {
mutators.Comment.edit = (action) => edit(context, action);
}
+2
View File
@@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators');
const Comment = require('./comment');
const Action = require('./action');
const Tag = require('./tag');
const User = require('./user');
const plugins = require('../../services/plugins');
@@ -12,6 +13,7 @@ let mutators = [
// Load in the core mutators.
Comment,
Action,
Tag,
User,
// Load the plugin mutators from the manager.
+39
View File
@@ -0,0 +1,39 @@
const TagsService = require('../../services/tags');
const errors = require('../../errors');
const {ADD_COMMENT_TAG, REMOVE_COMMENT_TAG} = require('../../perms/constants');
/**
* Modifies the targeted model with the specified operation to add/remove a tag.
*/
const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => {
// Get the global list of tags from the dataloader.
const tags = await Tags.getAll.load({id, item_type, asset_id});
// Resolve the TagLink that should be used to insert to the user. This will
// addtionally return with an ownership property that can be used to determine
// that the user who adds this tag must also be the owner of the resource.
let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type});
// Actually modify the tag on the model.
return operation(id, item_type, tagLink, ownership);
};
module.exports = (context) => {
let mutators = {
Tag: {
add: () => Promise.reject(errors.ErrNotAuthorized),
remove: () => Promise.reject(errors.ErrNotAuthorized)
}
};
if (context.user && context.user.can(ADD_COMMENT_TAG)) {
mutators.Tag.add = (tag) => modify(context, TagsService.add, tag);
}
if (context.user && context.user.can(REMOVE_COMMENT_TAG)) {
mutators.Tag.remove = (tag) => modify(context, TagsService.remove, tag);
}
return mutators;
};
+5
View File
@@ -1,3 +1,5 @@
const {decorateWithTags} = require('./util');
const Asset = {
recentComments({id}, _, {loaders: {Comments}}) {
return Comments.genRecentComments.load(id);
@@ -48,4 +50,7 @@ const Asset = {
}
};
// Decorate the Asset type resolver with a tags field.
decorateWithTags(Asset);
module.exports = Asset;
+5
View File
@@ -1,3 +1,5 @@
const {decorateWithTags} = require('./util');
const Comment = {
parent({parent_id}, _, {loaders: {Comments}}) {
if (parent_id == null) {
@@ -57,4 +59,7 @@ const Comment = {
}
};
// Decorate the Comment type resolver with a tags field.
decorateWithTags(Comment);
module.exports = Comment;
+4
View File
@@ -16,6 +16,8 @@ const RootMutation = require('./root_mutation');
const RootQuery = require('./root_query');
const Settings = require('./settings');
const Subscription = require('./subscription');
const TagLink = require('./tag_link');
const Tag = require('./tag');
const UserError = require('./user_error');
const User = require('./user');
const ValidationUserError = require('./validation_user_error');
@@ -39,6 +41,8 @@ let resolvers = {
RootQuery,
Settings,
Subscription,
TagLink,
Tag,
UserError,
User,
ValidationUserError,
+5 -6
View File
@@ -1,5 +1,4 @@
const wrapResponse = require('../helpers/response');
const CommentsService = require('../../services/comments');
const RootMutation = {
createComment(_, {comment}, {mutators: {Comment}}) {
@@ -35,12 +34,12 @@ const RootMutation = {
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
return wrapResponse(null)(Comment.setStatus({id, status}));
},
addCommentTag(_, {id, tag}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id)));
},
removeCommentTag(_, {id, tag}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id)));
addTag(_, {tag}, {mutators: {Tag}}) {
return wrapResponse(null)(Tag.add(tag));
},
removeTag(_, {tag}, {mutators: {Tag}}) {
return wrapResponse(null)(Tag.remove(tag));
}
};
module.exports = RootMutation;
+5
View File
@@ -0,0 +1,5 @@
const Tag = {
};
module.exports = Tag;
+11
View File
@@ -0,0 +1,11 @@
const {SEARCH_OTHER_USERS} = require('../../perms/constants');
const TagLink = {
assigned_by({assigned_by}, _, {user, loaders: {Users}}) {
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
return Users.getByID.load(assigned_by);
}
}
};
module.exports = TagLink;
+4
View File
@@ -1,3 +1,4 @@
const {decorateWithTags} = require('./util');
const KarmaService = require('../../services/karma');
const {
SEARCH_ACTIONS,
@@ -78,4 +79,7 @@ const User = {
}
};
// Decorate the User type resolver with a tags field.
decorateWithTags(User);
module.exports = User;
+18
View File
@@ -0,0 +1,18 @@
const {ADD_COMMENT_TAG} = require('../../perms/constants');
/**
* Decorates the typeResolver with the tags field.
*/
const decorateWithTags = (typeResolver) => {
typeResolver.tags = ({tags = []}, _, {user}) => {
if (user && user.can(ADD_COMMENT_TAG)) {
return tags;
}
return tags.filter((t) => t.tag.permissions.public);
};
};
module.exports = {
decorateWithTags
};
+96 -41
View File
@@ -69,6 +69,9 @@ type User {
# the current profiles of the user.
profiles: [UserProfile]
# the tags on the user
tags: [TagLink!]
# determines whether the user can edit their username
canEditName: Boolean
@@ -87,17 +90,6 @@ type User {
status: USER_STATUS
}
type Tag {
# the actual tag for the comment.
name: String!
# the user that assigned the tag. If NULL then the system automatically tagged it.
assigned_by: String
# the time when the tag was assigned.
created_at: Date!
}
# UsersQuery allows the ability to query users by a specific fields.
input UsersQuery {
action_type: ACTION_TYPE
@@ -112,6 +104,51 @@ input UsersQuery {
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
}
################################################################################
## Tags
################################################################################
# Used to represent the item type for a tag.
enum TAGGABLE_ITEM_TYPE {
# The action references a entity of type Asset.
ASSETS
# The action references a entity of type Comment.
COMMENTS
# The action references a entity of type User.
USERS
}
# Tag represents the underlying Tag that can be either stored in a global list
# or added uniquely to the entity.
type Tag {
# The actual name of the tag entry.
name: String!
# The time that this Tag was created.
created_at: Date!
}
# TagLink is used to associate a given Tag with a Model via a TagLink.
type TagLink {
# The underlying Tag that is either duplicated from the global list or created
# uniquely for this specific model.
tag: Tag!
# The user that assigned the tag. This TagLink could have been created by the
# system, in which case this will be null. It could also be null if the
# current user is not an Admin/Moderator.
assigned_by: User
# The date that the TagLink was created.
created_at: Date!
}
################################################################################
## Comments
################################################################################
@@ -223,7 +260,7 @@ type Comment {
body: String!
# the tags on the comment
tags: [Tag]
tags: [TagLink!]
# the user who authored the comment.
user: User
@@ -507,6 +544,9 @@ type Asset {
# The date that the asset was created.
created_at: Date
# the tags on the asset
tags: [TagLink!]
# The author(s) of the asset.
author: String
}
@@ -543,7 +583,7 @@ type ValidationUserError implements UserError {
}
################################################################################
## Queries;
## Queries
################################################################################
# Establishes the ordering of the content by their created_at time stamp.
@@ -625,7 +665,7 @@ type RootQuery {
interface Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# CreateCommentResponse is returned with the comment that was created and any
@@ -636,7 +676,7 @@ type CreateCommentResponse implements Response {
comment: Comment
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# Used to represent the item type for an action.
@@ -652,8 +692,13 @@ enum ACTION_ITEM_TYPE {
USERS
}
enum TAG_TYPE {
STAFF
input CreateLikeInput {
# The item's id for which we are to create a like.
item_id: ID!
# The type of the item for which we are to create the like.
item_type: ACTION_ITEM_TYPE!
}
# CreateCommentInput is the input content used to create a new comment.
@@ -669,7 +714,7 @@ input CreateCommentInput {
body: String!
# Tags
tags: [TAG_TYPE]
tags: [String]
}
@@ -697,7 +742,7 @@ type CreateFlagResponse implements Response {
flag: FlagAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
@@ -710,7 +755,7 @@ type CreateDontAgreeResponse implements Response {
dontagree: DontAgreeAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
input CreateDontAgreeInput {
@@ -756,7 +801,7 @@ input RejectUsernameInput {
type DeleteActionResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# SetUserStatusResponse is the response returned with possibly some errors
@@ -764,7 +809,7 @@ type DeleteActionResponse implements Response {
type SetUserStatusResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# SuspendUserResponse is the response returned with possibly some errors
@@ -772,7 +817,7 @@ type SetUserStatusResponse implements Response {
type SuspendUserResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# RejectUsernameResponse is the response returned with possibly some errors
@@ -780,7 +825,7 @@ type SuspendUserResponse implements Response {
type RejectUsernameResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# SetCommentStatusResponse is the response returned with possibly some errors
@@ -788,33 +833,43 @@ type RejectUsernameResponse implements Response {
type SetCommentStatusResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
# Response to addCommentTag mutation
type AddCommentTagResponse implements Response {
# An array of errors relating to the mutation that occured.
comment: Comment
errors: [UserError]
# ModifyTagInput is the input used to modify a tag.
input ModifyTagInput {
# name is the actual tag to add to the model.
name: String!
# id is the ID of the model in question that we are modifying the tag of.
id: ID!
# item_type is the type of item that we are modifying the tag if.
item_type: TAGGABLE_ITEM_TYPE!
# asset_id is used when the item_type is `COMMENTS`, the is needed to rectify
# the settings to get the asset specific tags/settings.
asset_id: ID
}
# Response to removeCommentTag mutation
type RemoveCommentTagResponse implements Response {
# Response to the addTag or removeTag mutations.
type ModifyTagResponse implements Response {
# An array of errors relating to the mutation that occured.
comment: Comment
errors: [UserError]
errors: [UserError!]
}
# Response to ignoreUser mutation
type IgnoreUserResponse implements Response {
# An array of errors relating to the mutation that occured.
errors: [UserError]
errors: [UserError!]
}
# Response to stopIgnoringUser mutation
type StopIgnoringUserResponse implements Response {
# An array of errors relating to the mutation that occured.
errors: [UserError]
errors: [UserError!]
}
# Input to editComment mutation.
@@ -831,7 +886,7 @@ type EditCommentResponse implements Response {
comment: Comment
# An array of errors relating to the mutation that occured.
errors: [UserError]
errors: [UserError!]
}
# All mutations for the application are defined on this object.
@@ -864,11 +919,11 @@ type RootMutation {
# Sets Comment status. Requires the `ADMIN` role.
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
# Add tag to comment.
addCommentTag(id: ID!, tag: String!): AddCommentTagResponse
# Add a tag.
addTag(tag: ModifyTagInput!): ModifyTagResponse!
# Remove tag from comment.
removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse
# Removes a tag.
removeTag(tag: ModifyTagInput!): ModifyTagResponse!
# Ignore comments by another user
ignoreUser(id: ID!): IgnoreUserResponse
+5 -4
View File
@@ -87,7 +87,7 @@ en:
custom_css_url_desc: "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external."
dashboard: Dashboard
days: Days
description: "As an admin you may customize the settings for the comment stream for this asset"
description: "As an admin, you can customize the settings for the comment stream for this story:"
domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Permitted Domains"
edit_comment_timeframe_heading: "Edit Comment Timeframe"
@@ -101,13 +101,13 @@ en:
enable_premod: "Enable Premoderation"
enable_premod_description: "Moderators must approve any comment before its published."
enable_premod_links_description: "Moderators must approve any comment containing a link before its published."
enable_questionbox: "Ask readers a question"
enable_questionbox: "Ask Readers a Question"
enable_questionbox_description: "This question will appear at the top of this comment stream. Ask readers about a certain issue in the article or pose discussion questions etc."
hours: Hours
include_comment_stream: "Include Comment Stream Description for Readers"
include_comment_stream_desc: "Write a message to be added to the top of your comment stream. Pose a topic include community guidelines etc."
include_text: "Include your text here."
include_question_here: "Write your question here."
include_question_here: "Write your question here:"
moderate: Moderate
moderation_settings: "Moderation Settings"
open: "Open"
@@ -202,7 +202,8 @@ en:
flag_reason: "Reason for reporting (Optional)"
flag_username: "Report username"
framework:
banned_account_msg: "Your account is currently banned. This means that you cannot Like, Report, or write comments. Please contact us if you have any questions."
banned_account_header: "Your account is currently banned."
banned_account_body: "This means that you cannot Like, Report, or write comments."
because_you_ignored: "Because you ignored the following commenters, their comments are hidden."
comment: comment
comment_is_ignored: "This comment is hidden because you ignored this user."
+2 -1
View File
@@ -202,7 +202,8 @@ es:
flag_reason: "Razón por la que hacer este reporte (Opcional)"
flag_username: "Reportar el nombre de usuario"
framework:
banned_account_msg: "Tu cuenta se encuentra suspendida. Esto significa que no puedes gustar, marcar o escribir comentarios."
banned_account_header: "Tu cuenta se encuentra suspendida."
banned_account_body: "Esto significa que no puedes gustar, marcar o escribir comentarios."
comment: "comentario"
comment_is_ignored: "Este comentario está escondido porque has ignorado al usuario."
comments: "comentarios"
+83
View File
@@ -0,0 +1,83 @@
const CommentModel = require('../models/comment');
module.exports = {
async up() {
// Find all comments that have tags.
let comments = await CommentModel.aggregate([
{$match: {
tags: {
$exists: true,
$ne: []
}
}},
{$project: {
id: true,
tags: true
}}
]);
// If no comments were found, nothing needes to be done!
if (comments.length <= 0) {
return;
}
// Create a new batch operation.
let batch = CommentModel.collection.initializeUnorderedBulkOp();
// Loop over the comments retrieved, updating the tag structure.
for (let {id, tags} of comments) {
// OLD
//
// [
// {
// name: 'OFF_TOPIC',
// assigned_by: '',
// created_at: new Date()
// }
// ]
// NEW
//
// [
// {
// tag: {
// name: 'OFF_TOPIC',
// permissions: {
// public: true,
// self: false,
// roles: []
// },
// models: ['COMMENTS'],
// created_at: new Date()
// },
// assigned_by: '',
// created_at: new Date()
// }
// ]
// Remap the tag structure.
tags = tags.map(({name, assigned_by, created_at}) => ({
tag: {
name,
permissions: {
public: true,
self: name === 'OFF_TOPIC', // at the time of migration, only off topic tags were self assigning
roles: []
},
models: ['COMMENTS'],
created_at
},
assigned_by,
created_at
}));
// Execute the batch operation.
batch.find({id}).updateOne({$set: {tags}});
}
// Execute the batch update operation.
await batch.execute();
}
};
+2 -11
View File
@@ -1,17 +1,8 @@
const mongoose = require('../services/mongoose');
const uuid = require('uuid');
const Schema = mongoose.Schema;
const ACTION_TYPES = [
'LIKE',
'FLAG'
];
const ITEM_TYPES = [
'ASSETS',
'COMMENTS',
'USERS'
];
const ACTION_TYPES = require('./enum/action_types');
const ITEM_TYPES = require('./enum/item_types');
const ActionSchema = new Schema({
id: {
+4
View File
@@ -1,6 +1,7 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./schema/tag_link');
const AssetSchema = new Schema({
id: {
@@ -47,6 +48,9 @@ const AssetSchema = new Schema({
default: null
},
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
+15 -31
View File
@@ -1,13 +1,8 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const TagLinkSchema = require('./schema/tag_link');
const uuid = require('uuid');
const STATUSES = [
'ACCEPTED',
'REJECTED',
'PREMOD',
'NONE'
];
const COMMENT_STATUS = require('./enum/comment_status');
/**
* The Mongo schema for a Comment Status.
@@ -16,7 +11,7 @@ const STATUSES = [
const StatusSchema = new Schema({
type: {
type: String,
enum: STATUSES,
enum: COMMENT_STATUS,
},
// The User ID of the user that assigned the status.
@@ -30,27 +25,6 @@ const StatusSchema = new Schema({
_id: false
});
/**
* The Mongo schema for a Comment Tag.
* @type {Schema}
*/
const TagSchema = new Schema({
name: String,
// The User ID of the user that assigned the status.
assigned_by: {
type: String,
default: null
},
created_at: {
type: Date,
default: Date
}
}, {
_id: false
});
/**
* A record of old body values for a Comment
*/
@@ -89,12 +63,14 @@ const CommentSchema = new Schema({
status_history: [StatusSchema],
status: {
type: String,
enum: STATUSES,
enum: COMMENT_STATUS,
default: 'NONE'
},
tags: [TagSchema],
parent_id: String,
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
@@ -110,6 +86,14 @@ const CommentSchema = new Schema({
},
});
// Add the indexes for the id of the comment.
CommentSchema.index({
'id': 1
}, {
unique: true,
background: false
});
CommentSchema.virtual('edited').get(function() {
return this.body_history.length > 1;
});
+4
View File
@@ -0,0 +1,4 @@
module.exports = [
'LIKE',
'FLAG'
];
+6
View File
@@ -0,0 +1,6 @@
module.exports = [
'ACCEPTED',
'REJECTED',
'PREMOD',
'NONE'
];
+5
View File
@@ -0,0 +1,5 @@
module.exports = [
'ASSETS',
'COMMENTS',
'USERS'
];
+4
View File
@@ -0,0 +1,4 @@
module.exports = [
'PRE',
'POST'
];
+5
View File
@@ -0,0 +1,5 @@
module.exports = [
'ADMIN',
'MODERATOR',
'STAFF'
];
+6
View File
@@ -0,0 +1,6 @@
module.exports = [
'ACTIVE',
'BANNED',
'PENDING',
'APPROVED' // Indicates that the users' username has been approved
];
+10
View File
@@ -0,0 +1,10 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const MigrationSchema = new Schema({
version: Number
});
const Migration = mongoose.model('Migration', MigrationSchema);
module.exports = Migration;
+53
View File
@@ -0,0 +1,53 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const ITEM_TYPES = require('../enum/item_types');
const USER_ROLES = require('../enum/user_roles');
/**
* The Mongo schema for a Tag.
* @type {Schema}
*/
const TagSchema = new Schema({
// The actual name of the tag.
name: String,
// Contains permission data.
permissions: {
// Determines if this tag is public or not.
public: {
type: Boolean,
default: true
},
// Determines if the owner of the Model can add/remove this tag on their own
// resources.
self: Boolean,
// Determines other roles that are allowed to set this tag on other
// resources.
roles: [{
type: String,
enum: USER_ROLES,
default: []
}]
},
// A list of all the model types that this tag can be added to.
models: [{
type: String,
enum: ITEM_TYPES
}],
// The date for when the tag was created.
created_at: {
type: Date,
default: Date
}
}, {
_id: false
});
module.exports = TagSchema;
+31
View File
@@ -0,0 +1,31 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const TagSchema = require('./tag');
/**
* The Mongo schema for linking a Tag to a Model.
* @type {Schema}
*/
const TagLinkSchema = new Schema({
// A deep copy of the tag that is the origin for this link. If the ID matches
// with existing tags in the global/asset context then content will be
// substituted.
tag: TagSchema,
// The User ID of the user that assigned the status.
assigned_by: {
type: String,
default: null
},
// The date when the tag was added to the model.
created_at: {
type: Date,
default: Date
}
}, {
_id: false
});
module.exports = TagLinkSchema;
+4 -7
View File
@@ -1,10 +1,7 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const MODERATION_OPTIONS = [
'PRE',
'POST'
];
const TagSchema = require('./schema/tag');
const MODERATION_OPTIONS = require('./enum/moderation_options');
/**
* SettingSchema manages application settings that get used on front and backend.
@@ -95,7 +92,8 @@ const SettingSchema = new Schema({
type: Number,
min: [0, 'Edit Comment Window length must be greater than zero'],
default: 30 * 1000,
}
},
tags: [TagSchema]
}, {
timestamps: {
createdAt: 'created_at',
@@ -127,4 +125,3 @@ SettingSchema.method('merge', function(src) {
const Setting = mongoose.model('Setting', SettingSchema);
module.exports = Setting;
module.exports.MODERATION_OPTIONS = MODERATION_OPTIONS;
+10 -16
View File
@@ -1,27 +1,20 @@
const mongoose = require('../services/mongoose');
const bcrypt = require('bcrypt');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./schema/tag_link');
const intersection = require('lodash/intersection');
const can = require('../perms');
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = [
'ADMIN',
'MODERATOR',
'STAFF'
];
const USER_ROLES = require('./enum/user_roles');
// USER_STATUS is the list of statuses that are permitted for the user status.
const USER_STATUS = [
'ACTIVE',
'BANNED',
'PENDING',
'APPROVED' // Indicates that the users' username has been approved
];
const USER_STATUS = require('./enum/user_status');
// ProfileSchema is the mongoose schema defined as the representation of a
// User's profile stored in MongoDB.
const ProfileSchema = new mongoose.Schema({
const ProfileSchema = new Schema({
// ID provides the identifier for the user profile, in the case of a local
// provider, the id would be an email, in the case of a social provider,
@@ -44,7 +37,7 @@ const ProfileSchema = new mongoose.Schema({
// used by the `local` provider to indicate when the email address was
// confirmed.
metadata: {
type: mongoose.Schema.Types.Mixed
type: Schema.Types.Mixed
}
}, {
_id: false
@@ -52,7 +45,7 @@ const ProfileSchema = new mongoose.Schema({
// UserSchema is the mongoose schema defined as the representation of a User in
// MongoDB.
const UserSchema = new mongoose.Schema({
const UserSchema = new Schema({
// This ID represents the most unique identifier for a user, it is generated
// when the user is created as a random uuid.
@@ -136,6 +129,9 @@ const UserSchema = new mongoose.Schema({
type: String,
}],
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// Additional metadata stored on the field.
metadata: {
default: {},
@@ -205,5 +201,3 @@ UserSchema.method('can', function(...actions) {
const UserModel = mongoose.model('User', UserSchema);
module.exports = UserModel;
module.exports.USER_ROLES = USER_ROLES;
module.exports.USER_STATUS = USER_STATUS;
+7 -1
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "1.8.0",
"version": "1.9.1",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"scripts": {
@@ -19,6 +19,11 @@
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build"
},
"talk": {
"migration": {
"minVersion": 1496771633
}
},
"config": {
"pre-git": {
"commit-msg": [],
@@ -110,6 +115,7 @@
"resolve": "^1.3.2",
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"snake-case": "^2.1.0",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"timekeeper": "^1.0.0",
"uuid": "^3.0.1",
+14
View File
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
+2
View File
@@ -0,0 +1,2 @@
export {addTag, removeTag} from 'coral-plugin-commentbox/actions';
export {addCommentClassName, removeCommentClassName} from 'coral-embed-stream/src/actions/stream';
@@ -0,0 +1,2 @@
export const commentBoxTagsSelector = (state) => state.commentBox.tags;
export const commentClassNamesSelector = (state) => state.stream.commentClassNames;
@@ -0,0 +1,2 @@
export {Slot} from 'coral-framework/components';
export {Icon} from 'coral-ui';
+1
View File
@@ -0,0 +1 @@
export {withReaction} from 'coral-framework/hocs';
+1
View File
@@ -0,0 +1 @@
export {t} from 'coral-framework/services/i18n';
View File
View File
+7 -4
View File
@@ -1,13 +1,16 @@
{
"server": [
"coral-plugin-respect",
"coral-plugin-auth",
"coral-plugin-like",
"coral-plugin-facebook-auth",
"coral-plugin-auth"
"coral-plugin-respect",
"coral-plugin-offtopic",
"coral-plugin-facebook-auth"
],
"client": [
"coral-plugin-respect",
"coral-plugin-like",
"coral-plugin-auth"
"coral-plugin-auth",
"coral-plugin-offtopic",
"coral-plugin-viewing-options"
]
}
@@ -60,7 +60,7 @@ type CreateLikeResponse implements Response {
like: LikeAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
type RootMutation {
@@ -2,7 +2,7 @@ import React from 'react';
import {Icon} from 'coral-ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
import {withReaction} from 'coral-plugin-api';
import {withReaction} from 'plugin-api/beta/client/hocs';
class LoveButton extends React.Component {
handleClick = () => {
@@ -60,7 +60,7 @@ type CreateLoveResponse implements Response {
love: LoveAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
type RootMutation {
@@ -0,0 +1,5 @@
import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants';
export const toggleCheckbox = () => ({
type: OFFTOPIC_TOGGLE_CHECKBOX
});
@@ -1,39 +1,45 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
import {t} from 'plugin-api/beta/client/services';
class OffTopicCheckbox extends React.Component {
export default class OffTopicCheckbox extends React.Component {
label = 'OFF_TOPIC';
handleChange = (e) => {
if (e.target.checked) {
this.props.addTag(this.label);
} else {
const idx = this.props.commentBox.tags.indexOf(this.label);
componentDidMount() {
this.clearTagsHook = this.props.registerHook('postSubmit', () => {
const idx = this.props.tags.indexOf(this.label);
this.props.removeTag(idx);
});
}
componentWillUnmount() {
this.props.unregisterHook(this.clearTagsHook);
}
handleChange = (e) => {
const {addTag, removeTag} = this.props;
if (e.target.checked) {
addTag(this.label);
} else {
const idx = this.props.tags.indexOf(this.label);
removeTag(idx);
}
}
render() {
return (
<div className={styles.offTopic}>
<label className={styles.offTopicLabel}>
<input type="checkbox" onChange={this.handleChange}/>
{t('off_topic')}
</label>
{
!this.props.isReply ? (
<label className={styles.offTopicLabel}>
<input type="checkbox" onChange={this.handleChange}/>
{t('off_topic')}
</label>
) : null
}
</div>
);
}
}
const mapStateToProps = ({commentBox}) => ({commentBox});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTag, removeTag}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
@@ -0,0 +1,32 @@
import React from 'react';
import styles from './styles.css';
export default class OffTopicFilter extends React.Component {
tag = 'OFF_TOPIC';
className = 'coral-plugin-off-topic-comment';
cn = {[this.className] : {tags: [this.tag]}};
handleChange = (e) => {
if (e.target.checked) {
this.props.addCommentClassName(this.cn);
this.props.toggleCheckbox();
} else {
const idx = this.props.commentClassNames.findIndex((i) => i[this.className]);
this.props.removeCommentClassName(idx);
this.props.toggleCheckbox();
}
this.props.closeViewingOptions();
}
render() {
return (
<div className={styles.viewingOption}>
<label>
<input type="checkbox" onChange={this.handleChange} checked={this.props.checked} />
Hide Off-Topic Comments
</label>
</div>
);
}
}
@@ -1,16 +1,13 @@
import React from 'react';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import t from 'coral-framework/services/i18n';
const isOffTopic = (tags) => {
return !!tags.filter((tag) => tag.name === 'OFF_TOPIC').length;
};
const isOffTopic = (tags) => !!tags.filter((t) => t.tag.name === 'OFF_TOPIC').length;
export default (props) => (
<span>
{
isOffTopic(props.comment.tags) ? (
isOffTopic(props.comment.tags) && props.depth === 0 ? (
<span className={styles.tag}>
{t('off_topic')}
</span>
@@ -8,11 +8,20 @@
}
.tag {
background: rgba(245, 188, 168, 0.6);
background: #3D73D5;
font-size: 12px;
font-weight: bold;
color: white;
display: inline-block;
margin: 0px 5px;
padding: 5px 5px;
border-radius: 2px;
}
.viewingOption {
padding: 5px 0;
}
:global(.coral-plugin-off-topic-comment) {
display: none;
}
@@ -0,0 +1 @@
export const OFFTOPIC_TOGGLE_CHECKBOX = 'OFFTOPIC_TOGGLE_CHECKBOX';
@@ -0,0 +1,14 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'plugin-api/alpha/client/actions';
import {commentBoxTagsSelector} from 'plugin-api/alpha/client/selectors';
import OffTopicCheckbox from '../components/OffTopicCheckbox';
const mapStateToProps = (state) => ({
tags: commentBoxTagsSelector(state)
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTag, removeTag}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
@@ -0,0 +1,30 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {toggleCheckbox} from '../actions';
import {commentClassNamesSelector} from 'plugin-api/alpha/client/selectors';
import OffTopicFilter from '../components/OffTopicFilter';
import {
closeViewingOptions
} from 'plugins/coral-plugin-viewing-options/client/actions';
import {
addCommentClassName,
removeCommentClassName
} from 'plugin-api/alpha/client/actions';
const mapStateToProps = (state) => ({
commentClassNames: commentClassNamesSelector(state),
checked: state.coralPluginOfftopic.checked
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
toggleCheckbox,
closeViewingOptions,
addCommentClassName,
removeCommentClassName
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicFilter);
+12 -3
View File
@@ -1,11 +1,20 @@
import OffTopicCheckbox from './components/OffTopicCheckbox';
import OffTopicTag from './components/OffTopicTag';
import translations from './translations.json';
import OffTopicTag from './components/OffTopicTag';
import OffTopicFilter from './containers/OffTopicFilter';
import OffTopicCheckbox from './containers/OffTopicCheckbox';
import reducer from './reducer';
/**
* coral-plugin-offtopic depends on coral-plugin-viewing-options
* in other to display filter and use the streamViewingOptions slot
*/
export default {
translations,
reducer,
slots: {
commentInputDetailArea: [OffTopicCheckbox],
commentInfoBar: [OffTopicTag]
commentInfoBar: [OffTopicTag],
viewingOptions: [OffTopicFilter]
}
};
@@ -0,0 +1,18 @@
import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants';
const initialState = {
checked: false
};
export default function offTopic (state = initialState, action) {
switch (action.type) {
case OFFTOPIC_TOGGLE_CHECKBOX: {
return {
...state,
checked: !state.checked
};
}
default :
return state;
}
}
+12 -4
View File
@@ -1,6 +1,14 @@
const {readFileSync} = require('fs');
const path = require('path');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8')
tags: [
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
};
@@ -1,4 +0,0 @@
## Extending TAG_TYPE by adding OFF_TOPIC Tag
enum TAG_TYPE {
OFF_TOPIC
}
@@ -44,7 +44,7 @@ type CreateRespectResponse implements Response {
respect: RespectAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
errors: [UserError!]
}
type RootMutation {
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}

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