diff --git a/.babelrc b/.babelrc index ca2e2cead..41d27bf34 100644 --- a/.babelrc +++ b/.babelrc @@ -1,16 +1,15 @@ { "sourceMaps": true, "presets": [ - "stage-0", "es2015" ], "plugins": [ - ["transform-decorators-legacy"], - ["transform-react-jsx"], - ["transform-object-assign"], - ["transform-class-properties"], - ["transform-async-to-generator"], - ["transform-object-rest-spread"], - ["transform-class-properties"] + "add-module-exports", + "transform-async-to-generator", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-react-jsx" ] } diff --git a/.eslintrc.json b/.eslintrc.json index d99b2254b..8b737cbd2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,7 +28,6 @@ "yoda": [1], "no-path-concat": [2], "eol-last": [1], - "no-continue": [1], "no-nested-ternary": [1], "no-tabs": [2], "no-unneeded-ternary": [1], diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fa7985c0..66ae8ade8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,95 +1,78 @@ -# Contribution Guide +# Contributor's Guide -We're very excited that you're interested in contributing to Talk! There is much to do. Before you begin, please review this document to get a sense of the practices and philosophies that hold this project together. +Welcome! We are very excited that you are interested in contributing to Talk. + +This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)! -## Doing the Work +## Product Roadmap -We are here to make it as seamless as possible to contribute to Talk. The following lists are meant to make it straightforward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content. +You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625. + +You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk. -### Code Reviews +## Contribute to the documentation +Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally. -One of the most valuable aspects of working in software. It is something that should challenge the reviewer and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants. +We are looking for _documentarians_ to: -Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand. +* make clarity, grammar and completeness updates, +* create new / missing sections, and +* take the lead in making sections, or the over all structure better. -Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace. +### But how? -**Is the code clear, efficient and a pleasure to read?** +* Our public docs site can be updated [here](https://github.com/coralproject/docs). +* [Let us know](https://github.com/coralproject/talk/wiki/Contact-Us) if you'd like permission to update our wiki. +* Update any of our .md docs files by following [this guide](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging). +* Our API docs need to be kept honest. [Update them here](https://github.com/coralproject/talk/blob/master/docs/swagger.yaml). -Somewhere at the intersection of good variable names, well laid out file structures, consistent formatting and appropriate comments lies beautiful code. Code is language spoken to at least two very distinct audiences, the computer that interprets it and the developer who encounters it. Both should be at the front of your mind when reviewing code. +## Integrate into your environment -Thinking like a computer, you could ask: +First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes! -* Is the code using memory efficiently? -* Is data being moved around unnecessarily? -* Are multiple network requests being made where fewer would do? -* Is there excess processing happening in a synchronous flow that may disrupt user experience? -* Are there large libraries included for small gains? +Talk is designed to integrate into existing environments in a variety of ways: -Then, returning to your human roots... Is the code readable? +* [Auth integrations](https://github.com/coralproject/talk/wiki/Security#authentication-strategies) +* [Push assets into Talk](https://github.com/coralproject/talk/blob/master/routes/api/assets/index.js) +* Monitoring Hooks (coming in 2017) -* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward? -* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices? -* Are the files separated logically such that each one contains a clear concept of code? +If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can. + +If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info. + +## Write some code + +First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes! + +### Build a New Feature / Plugin + +Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement. + +Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project. + +To get an idea of where the Coral Team is going, see: + +* our [product/design Trello board](https://trello.com/b/ILND751a/talk), +* our [current stories](https://www.pivotaltracker.com/n/projects/1863625), and +* our [issues](https://github.com/coralproject/talk/issues). -**Is the API documentation up to date? Are all client calls written against the docs?** +Examples: -We use [swagger](https://github.com/coralproject/talk/blob/master/swagger.yaml) to track our API documentation. - -* If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs. -* If client code calls APIs, are they written against the swagger.yml file? Are all return codes handled? - -**Is there sufficient test coverage?** - -Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests) - -* Can you a sense of the logic behind the code by reading the tests? -* Can you see both what should happen and what should _never, ever_ be allowed to happen? -* Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)? +* [Add An Emoji Button to Comments](https://github.com/coralproject/talk/wiki/Add-An-Emoji-Button-to-Comments) -### Forking, Branching and Merging +### Work on the Core -Talk follows the _master as tip_ repo structure. `master` is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added. +There is always more work to be done to make an application more stable, scaleable and secure. -Releases are _tagged_ off the master branch. +If you see issues in the code or have ideas on how we may improve Talk, please consider: -Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution. - -* At the outset of a piece of work, a branch or fork is made from master. -* The work is done in that fork. -* As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.) -* At least one other contributor to the project must review all code (see Code Reviews below.) -* If there are merge conflicts with master, merge master into the branch. -* Ensure that [circleci](https://circleci.com/) passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.) -* If merge conflicts exist with `master`, merge `master` into your branch and re-run CI before merging into master. -* Merge to master, but _you're not quite done yet!_ -* Deploy master to staging (or have a core member do so.) -* Ensure that all your changes are working on staging. -* Have your reviewer verify the same. -* ... aaaand the work is delivered! +* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging), +* [filing an issue](https://github.com/coralproject/talk/issues), or +* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us). -## Continuous Integration -We use circleci to run our ci: [https://circleci.com/gh/coralproject/talk](https://circleci.com/gh/coralproject/talk) - -Our pipeline will _test_, _lint_, and _build_ all pushes to the repo. - -Any branch not passing CI will not be merged into master. - -If you're working in a fork, please run each of the steps locally before submitting a PR. - - -## Coding Style - -### API Design - -When building APIs, we follow these principles: - -* Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations. -* Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object. -* Put non-required, flexible variables into query params, required/identity based values in request params. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 000000000..faee7bb25 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,95 @@ +# Installing a dev environment + +By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html). + +## Requirements + +### System + +- Any flavor of Linux, OSX or Windows +- 1GB memory (minimum) +- 5GB storage (minimum) + +### Software + +* [Node](https://nodejs.org/es/download/package-manager) v7 or later +* Mongo v3.2 or later +* Redis v3.2 or later + +_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_ + +## First time setup + +### Installation + +Navigate to a directory. + +``` +git clone https://github.com/coralproject/talk +cd talk +npm install +``` + +### Environmental Variables + +Talk uses environmental variables for configuration. You can learn about them in the [README file](README.md). + + +## Workflows + +### The server + +Starting the server: + +``` +npm start +``` + +Browse to `http://localhost:3000` (or your custom port.) + +### Building the front end + +Our build process will build all front end components registered [here](https://github.com/coralproject/talk/blob/6052cac1d3494f8060325a88bb2ce03c88c2f94c/webpack.config.dev.js#L9-L15). + +One time build: + +``` +npm build +``` + +Build, then rebuild when a file is updated (development build): + +``` +npm build-watch +``` + + +### Testing + +Run all tests once: + +` +npm test +` + +Run our end to end tests (will install Selenium and nightwatch): + +` +npm run e2e +` + +_Please ensure all tests are passing before submitting a PR!_ + +## Troubleshooting + + +##### Can't ping the redis server! + +- Check that Redis Server is running. +- Check that TALK_REDIS_URL is set. + +##### Authenticaiton doesn't work! + +- Make sure Redis is the correct version. + + diff --git a/README.md b/README.md index 0b77e95b7..239483628 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,57 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) -A commenting platform from The Coral Project. [https://coralproject.net](https://coralproject.net) + +A commenting platform from [The Coral Project](https://coralproject.net). ## Contributing to Talk -### Product Roadmap -You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625 +See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md)! -You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk +## Usage -### Local Dependencies -Node +### Installation -Mongo +To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md). -### Getting Started -`npm install` -Run it once to install the dependencies. +To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!! -`npm start` -Runs Talk. ### Configuration The Talk application requires specific configuration options to be available inside the environment in order to run, those variables are listed here: -- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to +- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database. +- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database. +- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to secure cookies. - `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook Login enabled app. - `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your Facebook Login enabled app. -- `TALK_ROOT_URL` (*required*) - root url of the installed application externally +- `TALK_ROOT_URL` (*required*) - root url of the installed application externally available in the format: `://` without the path. -- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name. +- `TALK_SMTP_EMAIL` (*required*) - the address to send emails from using the + SMTP provider. - `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using. - `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using. - `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`. - `TALK_SMTP_PORT` (*required*) - SMTP port. -### Running with Docker -Make sure you have Docker running first and then run `docker-compose up -d` -### Testing -`npm test` +### Install from Source -### Lint -`npm run lint` - -### Helpful URLs -Comment stream: http://localhost:3000/ - -Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html - -Moderator view: http://localhost:3000/admin - -### Docs -`swagger.yaml` +If you want to run Talk in development mode from source (without docker) you can read the [INSTALL file](INSTALL.md). ### License -**Apache-2.0** + + Copyright 2016 Mozilla Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + + See the License for the specific language governing permissions and limitations under the License. diff --git a/app.json b/app.json new file mode 100644 index 000000000..e498c5168 --- /dev/null +++ b/app.json @@ -0,0 +1,33 @@ +{ + "name": "The Coral Project: Talk", + "env": { + "TALK_SESSION_SECRET": { + "description": "The session secret", + "generator": "secret" + }, + "TALK_FACEBOOK_APP_ID": { + "value": "", + "required": true + }, + "TALK_FACEBOOK_APP_SECRET": { + "value": "", + "required": true + }, + "NODE_ENV": "production", + "TALK_SMTP_PORT": "2525", + "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN", + "NPM_CONFIG_PRODUCTION": "false" + }, + "addons": [{ + "plan": "mongolab:sandbox", + "as": "MONGO" + }, { + "plan": "rediscloud:30", + "as": "REDIS" + }, { + "plan": "postmark:10k", + "as": "POSTMARK" + }], + "image": "heroku/nodejs", + "success_url": "/admin/setup" +} diff --git a/bin/cli b/bin/cli index d8148282f..131614e86 100755 --- a/bin/cli +++ b/bin/cli @@ -1,10 +1,9 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; +// Perform rewrites to the runtime environment variables based on the contents +// of the process.env.REWRITE_ENV if it exists. This is done here as it is the +// entrypoint for the entire application. +require('env-rewrite').rewrite(); /** * Module dependencies. diff --git a/bin/cli-assets b/bin/cli-assets index 4b0d65c2a..008460f9e 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/bin/cli-jobs b/bin/cli-jobs index 91a0a6e59..60a7b8efb 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/bin/cli-serve b/bin/cli-serve index 5478b7c1e..05fe6efc7 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - const app = require('../app'); const debug = require('debug')('talk:server'); const http = require('http'); diff --git a/bin/cli-settings b/bin/cli-settings index e38249199..5fdfbc38f 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/bin/cli-users b/bin/cli-users index e212a477f..ae688e122 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 6d55d7b18..9c0f271cc 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -1,18 +1,20 @@ import React from 'react'; import {Router, Route, IndexRoute, browserHistory} from 'react-router'; -import ModerationQueue from 'containers/ModerationQueue/ModerationQueue'; +import ModerationContainer from 'containers/ModerationQueue/ModerationContainer'; import CommentStream from 'containers/CommentStream/CommentStream'; import Configure from 'containers/Configure/Configure'; +import Streams from 'containers/Streams/Streams'; import CommunityContainer from 'containers/Community/CommunityContainer'; import LayoutContainer from 'containers/LayoutContainer'; const routes = ( - + + ); diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js new file mode 100644 index 000000000..f431f1ad6 --- /dev/null +++ b/client/coral-admin/src/actions/assets.js @@ -0,0 +1,36 @@ +import { + FETCH_ASSETS_REQUEST, + FETCH_ASSETS_SUCCESS, + FETCH_ASSETS_FAILURE, + UPDATE_ASSET_STATE_REQUEST, + UPDATE_ASSET_STATE_SUCCESS, + UPDATE_ASSET_STATE_FAILURE +} from '../constants/assets'; +import coralApi from '../../../coral-framework/helpers/response'; + +/** + * Action disptacher related to assets + */ + +// Fetch a page of assets +// Get comments to fill each of the three lists on the mod queue +export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => { + dispatch({type: FETCH_ASSETS_REQUEST}); + return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) + .then(({result, count}) => + dispatch({type: FETCH_ASSETS_SUCCESS, + assets: result, + count + })) + .catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error})); +}; + +// Update an asset state +// Get comments to fill each of the three lists on the mod queue +export const updateAssetState = (id, closedAt) => (dispatch) => { + dispatch({type: UPDATE_ASSET_STATE_REQUEST}); + return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + .then(() => + dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) + .catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error})); +}; diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index d4aee034e..5d2104560 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -1,30 +1,71 @@ +import coralApi from '../../../coral-framework/helpers/response'; +import * as commentTypes from '../constants/comments'; +import * as actionTypes from '../constants/actions'; + +// Get comments to fill each of the three lists on the mod queue +export const fetchModerationQueueComments = () => { + return dispatch => { + dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); + return Promise.all([ + coralApi('/queue/comments/pending'), + coralApi('/comments?status=rejected'), + coralApi('/comments?action_type=flag') + ]) + .then(([pending, rejected, flagged]) => { + + /* Combine seperate calls into a single object */ + flagged.comments.forEach(comment => comment.flagged = true); + return { + comments: [...pending.comments, ...rejected.comments, ...flagged.comments], + users: [...pending.users, ...rejected.users, ...flagged.users], + actions: [...pending.actions, ...rejected.actions, ...flagged.actions] + }; + }) + .then(({comments, users, actions}) => { + + /* Post comments and users to redux store. Actions will be posted when they are needed. */ + dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); + dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments}); + dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions}); + + }); + }; +}; + +// Create a new comment +export const createComment = (name, body) => { + return dispatch => { + const comment = {body, name}; + return coralApi('/comments', {method: 'POST', comment}) + .then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res})) + .catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error})); + }; +}; + /** * Action disptacher related to comments */ -export const updateStatus = (status, id) => (dispatch, getState) => { - dispatch({type: 'COMMENT_STATUS_UPDATE', id, status}); - dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); +// Update a comment. Now to update a comment we need to send back the whole object +export const updateStatus = (status, comment) => { + return dispatch => { + dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_REQUEST, id: comment.id, status}); + return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}}) + .then(res => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_SUCCESS, res})) + .catch(error => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_FAILURE, error})); + }; }; export const flagComment = id => (dispatch, getState) => { - dispatch({type: 'COMMENT_FLAG', id}); + dispatch({type: commentTypes.COMMENT_FLAG, id}); dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); }; -export const createComment = (name, body) => dispatch => { - dispatch({type: 'COMMENT_CREATE', name, body}); -}; - // Dialog Actions export const showBanUserDialog = (userId, userName, commentId) => { - return dispatch => { - dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId}); - }; + return {type: commentTypes.SHOW_BANUSER_DIALOG, userId, userName, commentId}; }; export const hideBanUserDialog = (showDialog) => { - return dispatch => { - dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog}); - }; + return {type: commentTypes.HIDE_BANUSER_DIALOG, showDialog}; }; diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index 1dfda51b9..85ad5149e 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -10,6 +10,8 @@ export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING'; export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS'; export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; +export const WORDLIST_UPDATED = 'WORDLIST_UPDATED'; + export const fetchSettings = () => dispatch => { dispatch({type: SETTINGS_LOADING}); coralApi('/settings') @@ -21,10 +23,16 @@ export const fetchSettings = () => dispatch => { }); }; +// for updating top-level settings export const updateSettings = settings => { return {type: SETTINGS_UPDATED, settings}; }; +// this is a nested property, so it needs a special action. +export const updateWordlist = (listName, list) => { + return {type: WORDLIST_UPDATED, listName, list}; +}; + export const saveSettingsToServer = () => (dispatch, getState) => { let settings = getState().settings.toJS().settings; if (settings.charCount) { diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index eb5a02a55..bc42a7a4c 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,14 +1,15 @@ +import coralApi from '../../../coral-framework/helpers/response'; +import * as actions from '../constants/user'; /** * Action disptacher related to users */ -// -// export const banUser = (status, author_id) => (dispatch) => { -// dispatch({type: 'USER_STATUS_UPDATE', author_id, status}); -// }; -export const banUser = (status, userId, commentId) => { +// change status of a user +export const userStatusUpdate = (status, userId, commentId) => { return dispatch => { - dispatch({type: 'USER_BAN', status, userId, commentId}); - dispatch({type: 'MODERATION_QUEUE_FETCH'}); + dispatch({type: actions.UPDATE_STATUS_REQUEST}); + return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) + .then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res})) + .catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error})); }; }; diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 4a21b0846..17bd56714 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -8,6 +8,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations.json'; import {Icon} from 'react-mdl'; +import Highlighter from 'react-highlight-words'; import {FabButton, Button} from 'coral-ui'; const linkify = new Linkify(); @@ -30,8 +31,8 @@ export default props => {
{links ? Contains Link : null} -
- {props.actions.map((action, i) => getActionButton(action, i, props))} +
+ {props.modActions.map((action, i) => getActionButton(action, i, props))}
@@ -42,7 +43,9 @@ export default props => {
- {comment.body} +
@@ -63,21 +66,23 @@ const getActionButton = (action, i, props) => { if (action === 'ban') { return ( ); } return ( props.onClickAction(props.actionsMap[action].status, comment.id)} + onClick={() => props.onClickAction(props.actionsMap[action].status, comment)} /> ); }; diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 597ea62a6..b5eb40231 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -1,12 +1,11 @@ - -import React from 'react'; +import React, {PropTypes} from 'react'; import styles from './CommentList.css'; import key from 'keymaster'; import Hammer from 'hammerjs'; import Comment from 'components/Comment'; // Each action has different meaning and configuration -const actions = { +const modActions = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, @@ -15,6 +14,21 @@ const actions = { // Renders a comment list and allow performing actions export default class CommentList extends React.Component { + static propTypes = { + isActive: PropTypes.bool, + singleView: PropTypes.bool, + commentIds: PropTypes.arrayOf(PropTypes.string).isRequired, + comments: PropTypes.object.isRequired, + users: PropTypes.object.isRequired, + onClickAction: PropTypes.func, + + // list of actions (flags, etc) associated with the comments + modActions: PropTypes.arrayOf(PropTypes.string).isRequired, + loading: PropTypes.bool, + + suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired + } + constructor (props) { super(props); @@ -44,22 +58,22 @@ export default class CommentList extends React.Component { // Add swipe to approve or reject bindGestures () { - const {actions} = this.props; + const {modActions} = this.props; this._hammer = new Hammer(this.base); this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL}); - if (actions.indexOf('reject') !== -1) { + if (modActions.indexOf('reject') !== -1) { this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected')); } - if (actions.indexOf('approve') !== -1) { + if (modActions.indexOf('approve') !== -1) { this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved')); } } // Add key handlers. Each action has one and added j/k for moving around bindKeyHandlers () { - this.props.actions.filter(action => actions[action].key).forEach(action => { - key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status)); + this.props.modActions.filter(action => modActions[action].key).forEach(action => { + key(modActions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status)); }); key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down')); key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up')); @@ -122,22 +136,27 @@ export default class CommentList extends React.Component { } render () { - const {singleView, commentIds, comments, users, hideActive, key} = this.props; + const {singleView, commentIds, comments, users, hideActive, key, suspectWords} = this.props; const {active} = this.state; return ( -
    +
      {commentIds.map((commentId, index) => { const comment = comments[commentId]; const author = users[comment.author_id]; - return ; })} diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index e4d151d30..4c76424cc 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -16,6 +16,8 @@ export default ({handleLogout}) => ( activeClassName={styles.active}>{lang.t('configure.community')} {lang.t('configure.configure')} + {lang.t('configure.streams')}
        diff --git a/client/coral-admin/src/constants/actions.js b/client/coral-admin/src/constants/actions.js new file mode 100644 index 000000000..d64c62d26 --- /dev/null +++ b/client/coral-admin/src/constants/actions.js @@ -0,0 +1 @@ +export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS'; diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js new file mode 100644 index 000000000..0a2ecf73c --- /dev/null +++ b/client/coral-admin/src/constants/assets.js @@ -0,0 +1,6 @@ +export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST'; +export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; +export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE'; +export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST'; +export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS'; +export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE'; diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js index 856f619d0..35a915a1b 100644 --- a/client/coral-admin/src/constants/comments.js +++ b/client/coral-admin/src/constants/comments.js @@ -1,3 +1,12 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; -export const USER_BAN_SUCESS = 'USER_BAN_SUCESS'; +export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; +export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST'; +export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS'; +export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS'; +export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED'; +export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS'; +export const COMMENT_STATUS_UPDATE_SUCCESS = 'COMMENT_STATUS_UPDATE_SUCCESS'; +export const COMMENT_STATUS_UPDATE_FAILURE = 'COMMENT_STATUS_UPDATE_FAILURE'; +export const COMMENT_STATUS_UPDATE_REQUEST = 'COMMENT_STATUS_UPDATE_REQUEST'; +export const COMMENT_FLAG = 'COMMENT_FLAG'; diff --git a/client/coral-admin/src/constants/user.js b/client/coral-admin/src/constants/user.js new file mode 100644 index 000000000..8c0563237 --- /dev/null +++ b/client/coral-admin/src/constants/user.js @@ -0,0 +1,3 @@ +export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; +export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; +export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index e798266f0..286a7f364 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -7,7 +7,7 @@ import styles from './Community.css'; import Table from './Table'; import Loading from './Loading'; import NoResults from './NoResults'; -import Pager from './Pager'; +import Pager from 'coral-ui/components/Pager'; const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 050432743..6000c1d43 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -69,109 +69,115 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => { } }; -const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => { +const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => { if (fetchingSettings) { + /* maybe a spinner here at some point */ return

        Loading settings...

        ; } - return - - - - - -
        {lang.t('configure.enable-pre-moderation')}
        -

        - {lang.t('configure.enable-pre-moderation-text')} -

        -
        -
        - - - - - -
        {lang.t('configure.comment-count-header')}
        -

        - {lang.t('configure.comment-count-text-pre')} - - {lang.t('configure.comment-count-text-post')} - { - errors.charCount && - -
        - - {lang.t('configure.comment-count-error')} -
        - } -

        -
        -
        - - - - - - {lang.t('configure.include-comment-stream')} -

        - {lang.t('configure.include-comment-stream-desc')} -

        -
        -
        - - - - - - - - {lang.t('configure.close-after')} -
        - -
        - - - - - -
        -
        -
        - - - {lang.t('configure.closed-comments-desc')} - - - -
        ; + return ( +
        +

        {title}

        + + + + + + +
        {lang.t('configure.enable-pre-moderation')}
        +

        + {lang.t('configure.enable-pre-moderation-text')} +

        +
        +
        + + + + + +
        {lang.t('configure.comment-count-header')}
        +

        + {lang.t('configure.comment-count-text-pre')} + + {lang.t('configure.comment-count-text-post')} + { + errors.charCount && + +
        + + {lang.t('configure.comment-count-error')} +
        + } +

        +
        +
        + + + + + + {lang.t('configure.include-comment-stream')} +

        + {lang.t('configure.include-comment-stream-desc')} +

        +
        +
        + + + + + + + + {lang.t('configure.close-after')} +
        + +
        + + + + + +
        +
        +
        + + + {lang.t('configure.closed-comments-desc')} + + + +
        +
        + ); }; export default CommentSettings; diff --git a/client/coral-admin/src/containers/Configure/Configure.css b/client/coral-admin/src/containers/Configure/Configure.css index c0646c9e2..2b5c8d578 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -112,12 +112,12 @@ letter-spacing: 0.03em; } -#bannedWordlist { +#bannedWordlist, #suspectWordlist { width: 100%; padding: 10px; } -.bannedWordHeader { +.wordlistHeader { font-weight: bold; font-size:18px; margin-bottom:3px; diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index ed8ba9b26..9fc24ad53 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -1,6 +1,11 @@ import React from 'react'; import {connect} from 'react-redux'; -import {fetchSettings, updateSettings, saveSettingsToServer} from '../../actions/settings'; +import { + fetchSettings, + updateSettings, + saveSettingsToServer, + updateWordlist, +} from '../../actions/settings'; import { List, ListItem, @@ -14,6 +19,7 @@ import translations from '../../translations.json'; import EmbedLink from './EmbedLink'; import CommentSettings from './CommentSettings'; import Wordlist from './Wordlist'; +import has from 'lodash/has'; class Configure extends React.Component { constructor (props) { @@ -21,7 +27,6 @@ class Configure extends React.Component { this.state = { activeSection: 'comments', - wordlist: [], changed: false, errors: {} }; @@ -31,15 +36,6 @@ class Configure extends React.Component { this.props.dispatch(fetchSettings()); } - componentWillUpdate = (newProps) => { - if ((!this.props.settings - || !this.props.settings.wordlist) - && newProps.settings.wordlist - && newProps.settings.wordlist.length !== 0 ) { - this.setState({wordlist: newProps.settings.wordlist.join(', ')}); - } - } - saveSettings = () => { this.props.dispatch(saveSettingsToServer()); this.setState({changed: false}); @@ -49,15 +45,9 @@ class Configure extends React.Component { this.setState({activeSection}); } - onChangeWordlist = (event) => { - event.preventDefault(); - const newlist = event.target.value; - this.setState({wordlist: newlist.toLowerCase(), changed: true}); - this.props.dispatch(updateSettings({ - wordlist: newlist.toLowerCase() - .split(',') - .map((word) => word.trim()) - })); + onChangeWordlist = (listName, list) => { + this.setState({changed: true}); + this.props.dispatch(updateWordlist(listName, list)); } onSettingUpdate = (setting) => { @@ -74,46 +64,46 @@ class Configure extends React.Component { }); } - getSection = (section) => { + getSection (section) { + const pageTitle = this.getPageTitle(section); switch(section){ case 'comments': return ; case 'embed': - return ; + return ; case 'wordlist': - return ; + return has(this, 'props.settings.wordlist') + ? + :

        loading wordlists

        ; } } - getPageTitle = (section) => { + getPageTitle (section) { switch(section) { case 'comments': return lang.t('configure.comment-settings'); case 'embed': return lang.t('configure.embed-comment-stream'); - case 'wordlist': - return lang.t('configure.wordlist'); + default: + return ''; } } render () { - let pageTitle = this.getPageTitle(this.state.activeSection); const section = this.getSection(this.state.activeSection); const showSave = Object.keys(this.state.errors).reduce( (bool, error) => this.state.errors[error] ? false : bool, this.state.changed); - if (this.props.fetchingSettings) { - pageTitle += ' - Loading...'; - } - return (
        @@ -151,7 +141,6 @@ class Configure extends React.Component {
        -

        {pageTitle}

        { this.props.saveFetchingError } { this.props.fetchSettingsError } { section } diff --git a/client/coral-admin/src/containers/Configure/EmbedLink.js b/client/coral-admin/src/containers/Configure/EmbedLink.js index 6c5c3da99..374d78ea3 100644 --- a/client/coral-admin/src/containers/Configure/EmbedLink.js +++ b/client/coral-admin/src/containers/Configure/EmbedLink.js @@ -31,16 +31,21 @@ class EmbedLink extends Component { render () { const embedText = `
        `; - return - -

        {lang.t('configure.copy-and-paste')}

        -