Merge branch 'edit-comment' of github.com:gobengo/talk into edit-comment

This commit is contained in:
Benjamin Goering
2017-05-09 15:03:19 -07:00
19 changed files with 390 additions and 39 deletions
+36 -2
View File
@@ -42,9 +42,43 @@ available in the format: `<scheme>://<host>` without the path.
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
### License
## Supported Browsers
Copyright 2016 Mozilla Foundation
### Web
- Chrome: latest 2 versions
- Firefox: latest 2 versions, and most recent extended support version, if any
- Safari: latest 2 versions
- Internet Explorer: IE Edge, 11
### iOS Devices
- iPad
- iPad Pro
- iPhone 6 Plus
- iPhone 6
- iPhone 5
### iOS Browsers
- Chrome for iOS: latest version
- Firefox for iOS: latest version
- Safari for iOS: latest version
### Android Devices
- Galaxy S5
- Nexus 5X
- Nexus 6P
### Android Browsers
- Chrome for Android: latest version
- Firefox for Android: latest version
## License
Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
import {Router, Route, IndexRedirect, browserHistory} from 'react-router';
import Stories from 'containers/Stories/Stories';
import Configure from 'containers/Configure/Configure';
@@ -18,7 +18,7 @@ const routes = (
<div>
<Route exact path="/admin/install" component={InstallContainer}/>
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={Dashboard} />
<IndexRedirect to='/admin/moderate/all' />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
+1 -1
View File
@@ -4,7 +4,7 @@ export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
// Ban User Dialog
export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote});
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
// hide shortcuts note
@@ -8,14 +8,14 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => {
const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => {
e.preventDefault();
handleBanUser({userId})
.then(handleClose)
.then(() => rejectComment({commentId}));
.then(() => commentStatus === 'REJECTED' ? null : rejectComment({commentId}));
};
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, showRejectedNote}) => (
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, commentStatus, showRejectedNote}) => (
<Dialog
className={styles.dialog}
id="banuserDialog"
@@ -36,7 +36,7 @@ const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, c
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, handleBanUser, rejectComment, handleClose)} raised>
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, commentStatus, handleBanUser, rejectComment, handleClose)} raised>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -193,10 +193,12 @@
box-shadow: none;
color: white;
background-color: #519954;
cursor: not-allowed;
}
.reject__active, .rejected__active {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
@@ -6,7 +6,6 @@ import {getMetrics} from 'coral-admin/src/graphql/queries';
import FlagWidget from './FlagWidget';
import ActivityWidget from './ActivityWidget';
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
import {Spinner} from 'coral-ui';
@@ -43,12 +42,7 @@ const mapStateToProps = state => {
};
};
const mapDispatchToProps = dispatch => ({
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(mapStateToProps),
getMetrics
)(Dashboard);
@@ -43,12 +43,13 @@ class ModerationContainer extends Component {
const {acceptComment, rejectComment} = this.props;
const {selectedIndex} = this.state;
const comments = this.getComments();
const commentId = {commentId: comments[selectedIndex].id};
const comment = comments[selectedIndex];
const commentId = {commentId: comment.id};
if (accept) {
acceptComment(commentId);
comment.status !== 'ACCEPTED' && acceptComment(commentId);
} else {
rejectComment(commentId);
comment.status !== 'REJECTED' && rejectComment(commentId);
}
}
@@ -185,6 +186,7 @@ class ModerationContainer extends Component {
open={moderation.banDialog}
user={moderation.user}
commentId={moderation.commentId}
commentStatus={moderation.commentStatus}
handleClose={props.hideBanUserDialog}
handleBanUser={props.banUser}
showRejectedNote={moderation.showRejectedNote}
@@ -212,7 +214,7 @@ const mapDispatchToProps = dispatch => ({
singleView: () => dispatch(singleView()),
updateAssets: assets => dispatch(updateAssets(assets)),
fetchSettings: () => dispatch(fetchSettings()),
showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)),
showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
hideShortcutsNote: () => dispatch(hideShortcutsNote()),
});
@@ -18,7 +18,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
const Comment = ({actions = [], comment, ...props}) => {
const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) => {
const links = linkify.getMatches(comment.body);
const linkText = links ? links.map(link => link.raw) : [];
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
@@ -30,6 +30,13 @@ const Comment = ({actions = [], comment, ...props}) => {
commentType = 'flagged';
}
// since words are checked against word boundaries on the backend,
// this should be the behavior on the front end as well.
// currently the highlighter plugin does not support this out of the box.
const searchWords = [...suspectWords, ...bannedWords].filter(w => {
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
}).concat(linkText);
return (
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
<div className={styles.container}>
@@ -41,7 +48,7 @@ const Comment = ({actions = [], comment, ...props}) => {
<span className={styles.created}>
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')} />
<CommentType type={commentType} />
</div>
{comment.user.status === 'banned' ?
@@ -60,8 +67,8 @@ const Comment = ({actions = [], comment, ...props}) => {
<div className={styles.itemBody}>
<p className={styles.body}>
<Highlighter
searchWords={[...props.suspectWords, ...props.bannedWords, ...linkText]}
textToHighlight={comment.body} />
searchWords={searchWords}
textToHighlight={comment.body} /> <a className={styles.external} href={`${comment.asset.url}#${comment.id}`} target="_blank"><Icon name='open_in_new' /> {lang.t('comment.view_context')}</a>
</p>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
@@ -74,8 +81,8 @@ const Comment = ({actions = [], comment, ...props}) => {
user={comment.user}
status={comment.status}
active={active}
acceptComment={() => props.acceptComment({commentId: comment.id})}
rejectComment={() => props.rejectComment({commentId: comment.id})} />;
acceptComment={() => comment.status === 'ACCEPTED' ? null : props.acceptComment({commentId: comment.id})}
rejectComment={() => comment.status === 'REJECTED' ? null : props.rejectComment({commentId: comment.id})} />;
})}
</div>
</div>
@@ -106,6 +113,7 @@ Comment.propTypes = {
}),
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
})
})
@@ -28,12 +28,6 @@ const ModerationMenu = (
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {lang.t('modqueue.all')} <CommentCount count={allCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('premod')}
className={`mdl-tabs__tab ${styles.tab}`}
@@ -46,6 +40,12 @@ const ModerationMenu = (
activeClassName={styles.active}>
<Icon name='flag' className={styles.tabIcon} /> {lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}
@@ -423,3 +423,24 @@ span {
position: relative;
top: 7px;
}
.external {
font-size: .7em;
text-decoration: none;
color: #063b9a;
cursor: pointer;
font-weight: normal;
margin-left: 10px;
white-space: nowrap;
&:hover {
text-decoration: underline;
opacity: .9;
}
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
@@ -11,6 +11,7 @@ fragment commentView on Comment {
asset {
id
title
url
}
action_summaries {
count
@@ -6,6 +6,7 @@ const initialState = Map({
modalOpen: false,
user: Map({}),
commentId: null,
commentStatus: null,
banDialog: false,
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show'
});
@@ -14,12 +15,14 @@ export default function moderation (state = initialState, action) {
switch (action.type) {
case actions.HIDE_BANUSER_DIALOG:
return state
.set('banDialog', false);
.set('banDialog', false)
.set('commentStatus', null);
case actions.SHOW_BANUSER_DIALOG:
return state
.merge({
user: Map(action.user),
commentId: action.commentId,
commentStatus: action.commentStatus,
showRejectedNote: action.showRejectedNote,
banDialog: true
});
+2
View File
@@ -72,6 +72,7 @@
"flagged": "flagged",
"anon": "Anonymous",
"ban_user": "Ban User",
"view_context": "View context",
"banned_user": "Banned User"
},
"user": {
@@ -259,6 +260,7 @@
"comment": {
"flagged": "marcado",
"anon": "Anónimo",
"view_context": "Ver contexto",
"ban_user": "Suspender Usuario",
"banned_user": "Usuario Suspendido"
},
@@ -3,10 +3,8 @@
}
.apply {
position: absolute;
top: 38%;
transform: translateX(-50%);
right: 0;
float: right;
margin: 0 10px;
}
.wrapper ul {
@@ -12,7 +12,6 @@ export default ({handleChange, handleApply, changed, ...props}) => (
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{lang.t('configureCommentStream.title')}</h3>
<p>{lang.t('configureCommentStream.description')}</p>
<Button
type="submit"
className={styles.apply}
@@ -20,6 +19,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
cStyle={changed ? 'green' : 'darkGrey'} >
{lang.t('configureCommentStream.apply')}
</Button>
<p>{lang.t('configureCommentStream.description')}</p>
</div>
<ul>
<li>
+102
View File
@@ -0,0 +1,102 @@
# Experimental plugins
Talk plugins are, in essence, small programs that hook into the core application in a variety of ways. Ultimately, this code can do anything that javascript is capable of. In addition, plugins can import any core code to hook into talk at any level.
If you want to write plugins that integrate with core code beyond the api described in [PLUGINS.md](PLUGINS.md), please keep the following things in mind:
* core code may change and break your plugin
* you may introduce inefficiencies with your plugin that could hurt performance/crash Talk
* you may cause bugs in other areas of Talk
If you'd like to build a supported plugin but don't have the hooks you need, please file an issue on this repo and we can discuss deepening the supported plugin api!
With that said, here's some of the prime experimental integration points:
## Reducers and Actions : Redux
Talk is powered by Redux and our plugins can too! Our plugins can have their own reducers and actions.
```js
import MyButton from './MyButton';
import reducer from './reducer';
export default {
slots: {
commentDetail: [MyButton],
},
reducer
};
```
## Import Actions from Talk
We can easily trigger `Talk` actions in our plugin Components.
```js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
class MyButton extends Component {
render() {
return <button onClick={this.props.addTag('MY_TAG')}>My Button</button>;
}
}
const mapStateToProps = ({commentBox}) => ({commentBox});
const mapDispatchToProps = dispatch =>
bindActionCreators({addTag, removeTag}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
```
## ESlint and Babel
In talk we use `eslint:recommended` and Babel with the latest ECMAScript Features. But you can use your own!
While building your plugin you need to specify a `.eslintrc.json` file and a`.babelrc` file.
#### `.eslintrc.json`
```json
{
"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"] }]
}
}
````
#### `. babelrc `
```json
{
"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"
]
}
````
+176
View File
@@ -0,0 +1,176 @@
# Plugins
We can build plugins to extend the functionality of Talk.
This guide is a walkthrough of our plugin architecture and components that we provide that allow you to build on top of Core coral components without having to understand the concepts there in. It is organized into three sections:
* Plugin architecture
* Using our building block components
* Styling
Advanced users will quickly realize that our plugins have complete access to core code. If you would like to write advanced plugins that reach outside of our published API as described in this document, please see [our notes on experimental pluginss](PLUGINS-experimental.md).
Under the hood our plugins are powered by *React*, *Redux* and *GraphQL*. We can also build them with simple vanilla javascript.
## Plugin Architecture
The plugins live in the `/plugins` folder. Each plugin must have an `index.js` file and two folders `client` and `server`.
### The Client Folder
The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin.
```
my-plugin/
├── client/
│ └── index.js <-- index for client side functionality
├── server/
└── index.js <-- base plugin index
```
For now our base plugin `index.js` file should look like this:
```js
export default {
// We will add more here later.
};
```
### Creating a Component
We can add our components (or any other javascript code) within the `client` folder.
```
my-plugin/
├── client/
│ ├── MyComponent.js
│ └── index.js
├── server/
└── index.js
```
Our component could look like this:
```js
import React, {Component} from 'react';
class MyButton extends Component {
render() {
return <button>My Button</button>;
}
}
export default MyButton;
```
Here we create a component that renders a `button`. Now that we created our component we need to specify where it should get injected within Talk!
To tell Talk where that Component should get injected we need to specify which *Slots* to insert it into.
```js
import React from 'react';
export default = () => <button>My Button</button>;
```
### Slots
In Talk we have defined specific *Slots* where we can inject components.
Here is how we specify our slots config in `my-plugin/index.js`
```js
import MyButton from './MyButton';
export default {
slots: {
commentDetail: [MyButton]
}
};
```
Here Im specifying that the MyComponent Component will take place within the `commentDetail` in Talk.
`commentDetail` its a specific slot in the CommentStream. It means that it will be embedded inside de comment detail.
Slots properties take an`Array` so we can add as many components as we want.
## Building Blocks (TBD)
`Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.`
In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order components that expose a simple api.
### Reactions
Reactions provide users the ability to 'like', 'respect', etc... comments.
Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples.
### Comment Stream
Comment streams may be created with filtering and ordering in place:
* filter by user
* filter by tag
* sort by date ascending / descending
### Comment Commit hooks
// docs for the pre/post comment submit commit hooks
### Mod Queues
Moderation queues can be added via configuration objects passed in through plugins.
Basic mod queues will resemble the current moderation queues but can be generated from different lists of comments.
* filter by user tag
* filter by comment tag
* filter by comment status
* Custom queries (paired with back end plugins that provide queries to get the data)
#### Advanced mod queues
Advanced mod queues can be created giving plugin authors the power to create the cards that appear in the queue, create actions and custom buttons, etc...
### Custom Configuration
Plugins may rely on configuration options that admins/moderators can set in the Configuration section.
Basic settings can be added via json configuration in a plugin.
* Setting headline
* Setting description
* Setting input type
* Default value
* Variable name
#### Advanced Custom Configuration (low prioritiy)
Users can inject configuration interfaces that they create into the configuration allowing for more advanced configuration.
## Styling Plugins
Talk uses CSS Modules. This basically means that you can also add your CSS Module to your plugin without colliding with the rest of Talk!
##### My Component
```js
import styles from './style.css';
class MyCoralButton extends Component {
render() {
return <button className={styles.button}>My Button</button>;
}
}
````
Our `style.css` should could look like this.
```css
.button {
background: coral;
border-radius: 3px;
}
```
### The server folder and the index file
Read more about the `/server` and how to extend Talk here.
[talk/PLUGINS.md at master · coralproject/talk · GitHub](https://github.com/coralproject/talk/blob/master/PLUGINS.md)
+1 -1
View File
@@ -2,4 +2,4 @@ const {RedisPubSub} = require('graphql-redis-subscriptions');
const {connectionOptions} = require('../services/redis');
module.exports = new RedisPubSub(connectionOptions);
module.exports = new RedisPubSub({connection: connectionOptions});
+8
View File
@@ -136,6 +136,14 @@ const UserSchema = new mongoose.Schema({
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
},
toJSON: {
transform: function (doc, ret) {
delete ret.password;
delete ret._id;
delete ret.__v;
}
}
});