mirror of
https://github.com/wassname/talk.git
synced 2026-08-09 12:30:42 +08:00
merge master
This commit is contained in:
+2
-1
@@ -1 +1,2 @@
|
||||
dist
|
||||
dist
|
||||
client/lib
|
||||
|
||||
+15
-26
@@ -5,25 +5,15 @@
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
@@ -35,7 +25,6 @@
|
||||
"no-throw-literal": [2],
|
||||
"yoda": [1],
|
||||
"no-path-concat": [2],
|
||||
"no-process-exit": [2],
|
||||
"eol-last": [1],
|
||||
"no-continue": [1],
|
||||
"no-nested-ternary": [1],
|
||||
@@ -46,20 +35,20 @@
|
||||
"no-const-assign": [2],
|
||||
"no-duplicate-imports": [2],
|
||||
"prefer-template": [1],
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
}],
|
||||
"no-var": [2],
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-unused-vars": ["error", { "argsIgnorePattern": "next" }],
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{"max": 1}
|
||||
],
|
||||
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }]
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "next"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
npm-debug.log*
|
||||
dist
|
||||
!dist/coral-admin
|
||||
dist/coral-admin/bundle.js
|
||||
tests/e2e/reports
|
||||
.DS_Store
|
||||
*.iml
|
||||
*.swp
|
||||
dump.rdb
|
||||
.env
|
||||
gaba.cfg
|
||||
.idea/
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Contribution 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.
|
||||
|
||||
|
||||
## Doing the Work
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### Code Reviews
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace.
|
||||
|
||||
**Is the code clear, efficient and a pleasure to read?**
|
||||
|
||||
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.
|
||||
|
||||
Thinking like a computer, you could ask:
|
||||
|
||||
* 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?
|
||||
|
||||
Then, returning to your human roots... Is the code readable?
|
||||
|
||||
* 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?
|
||||
|
||||
|
||||
**Is the API documentation up to date? Are all client calls written against the docs?**
|
||||
|
||||
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...)?
|
||||
|
||||
|
||||
### Forking, Branching and Merging
|
||||
|
||||
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.
|
||||
|
||||
Releases are _tagged_ off the master branch.
|
||||
|
||||
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!
|
||||
|
||||
|
||||
## 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.
|
||||
@@ -3,8 +3,14 @@ A commenting platform from The Coral Project. [https://coralproject.net](https:/
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
### Product Roadmap
|
||||
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
|
||||
|
||||
### Local Dependencies
|
||||
Node
|
||||
|
||||
Mongo
|
||||
|
||||
### Getting Started
|
||||
@@ -19,13 +25,19 @@ Runs Talk.
|
||||
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
|
||||
secure cookies
|
||||
- `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.
|
||||
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 available in the format: `<scheme>://<host>` without the path.
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name.
|
||||
- `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`
|
||||
@@ -37,9 +49,11 @@ Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
`npm run lint`
|
||||
|
||||
### Helpful URLs
|
||||
Bare comment stream: http://localhost:5000/client/coral-embed-stream/
|
||||
Comment stream embedded on sample article: http://localhost:5000/client/coral-embed-stream/samplearticle.html
|
||||
Moderator view: http://localhost:5000/admin/
|
||||
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`
|
||||
|
||||
@@ -22,7 +22,10 @@ if (app.get('env') !== 'test') {
|
||||
//==============================================================================
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
app.use(helmet());
|
||||
// We disable frameward on helmet to allow crossdomain injection of the embed
|
||||
app.use(helmet({
|
||||
frameguard: false
|
||||
}));
|
||||
app.use(bodyParser.json());
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
@@ -45,7 +48,7 @@ const session_opts = {
|
||||
},
|
||||
store: new RedisStore({
|
||||
ttl: 1800,
|
||||
client: redis,
|
||||
client: redis.createClient(),
|
||||
})
|
||||
};
|
||||
|
||||
@@ -91,7 +94,9 @@ app.use((req, res, next) => {
|
||||
// returning a status code that makes sense.
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
if (app.get('env') !== 'test') {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36" version="6.0.1.8" editor="www.draw.io" type="device"><diagram name="Page-1">7Vpbc6M2FP41nrYP6wFksP1oO0k7nd1O2nTa7qMMCtZGICpE4vTXVwIJkCVnnQbbWU/z4MDRBen7zk0HRmCVbX9ksNh8ogkio8BLtiNwNQqC6dwTv1Lw3AjCYNoIUoaTRuR3gjv8D1JCNS6tcIJKoyOnlHBcmMKY5jmKuSGDjNEns9s9JeZTC5giS3AXQ2JL/8QJ3zTSWeh18p8QTjf6yb6nWtYwfkgZrXL1vFEA7uu/pjmDei7Vv9zAhD71ROB6BFaMUt5cZdsVIhJaDVsz7mZPa7tuhnJ+yIBJ1Ix4hKRCesn1wvizBkOMELiLm6VYbSGFMaGVmGD5tMEc3RUwlsInoQlCtuEZEXe+uFRTI8bRdu/6/HbXQpkQzRBnz6KLGhBpjdCK1Nw9daQEGvpNn5C56giVIqTtzB0Y4kLh4cYGOKBIhJqoW8r4hqY0h+S6ky5r7pGcwTPBQFvM/5LicajuPuuWXCys1yRvP6sJSg4ZX0iNrmGHZYljLb7BpJ08T+xOQtjr8gVx/qwsDVacClG3g4+UFqpfyRl9QCtKKKv3DLz6r23R9tARbLEpIKMVixVmCkWx4hSpXoHSOgnni1rAEIEcP5pW+RZOw/kxOPV7jHrjaWiQalA6Bt8IqTvU/BeWQ+9EpEb+8UkNTFLHfmTyGrbtt4hhsQXEviETHoDtJpKcgO3AiliLohCCO8REqBkFERErWib4UVym8vL7X0R6MgpWcp/bgqGyHH8pf9AdxeN6fS1FOijEOdCyot7eEAemZojzdZbRC3L+xBXkZt7b0Zx8Pf4fH4I2fVIQABcEMxcEkwEgiCbHdx8vhYTatVyil/AnjtB/Ii/huxTbcg2LJMP5SObIN+JXHmOYWAbN9zkHewLpet6JI/F1dqwdSTCzrShwWFE0hBGF5zWi6cUaUXhGIwotI1pRBmWX62yNkvIdaDmYOWLFsbTcn9vJRyWeGni3jD5i4T7OAUmkE2Bt+L59TPY9YEMCBoCkXW4HySeIiUrIsNj1OQAJDECmDhVxlQ2GwCME53WEl+oHdc3gLCfMwC6T/UzXwta9XyskJI5Dx0OF3tcxY7aTYx/qN6cDGAVwBBKCxWa+kxjeib1biIhtcRMEU8dymkvLuRe6uiOCBKe51GkxvzyFLyVIOIZkoRoynCS12blQ34Pra4qWhxxnpg6kgyGQjo7hfuyi5WW4mDe4DjX0lmIxY0t9sHOYB7ucNi5Njepoff1EjR+0Jqr1o93PYSWAo5TPTJVpg5Sjzn056mRGLDC3I1ZzLD1BxJrYdZz/X14ci9TwRKQC+/hxw2jtNq7zRJ5CSJXivHTlIwzBWHS8YSiptoemJfqFXxOLE1qt6xbf0BV/TyQd9k3g7rkvmJzw3DeZWsCrYpFQNFU+imAmd52vy8JdK1rRLKtyzJ/39f1qtekTzGHa1bgPJK/KyCKWC+0yoY9wjcgtLXFd7AJXa8o5zUQHIhuW7UvsniWp19h2NsXpDtntO21pd4V+GyKVA+epEg+gEMAMkD6w9QEcSx1mdjrbI9eiDWXrujLwrmkbnhLgOHcfixKdYvco+Q0l2C7HaHgTyGEp4N13Dhj4O4adigSIbGjmcxsa3x8AG/2sgXMBqyRxifE+1J8fnaH8GjpKazRP6dV+p3FqrTYNPnAWHl3v7QZR68CCx3C+PXyivyv5RdVSFhU+qPrAQvQg6J53rbuR975JrexpZMOHslZPOYs/Kbb2LLpS/jskD+LfgsWSj5hXjfgKw5TBrBcUmseZS3ghJTjz9v4QLWNfUkiKDXzlNi6p1mNmIbZbb2VvrDTL3L39LrA55nffXoLrfwE=</diagram></mxfile>
|
||||
@@ -19,7 +19,10 @@ const pkg = require('../package.json');
|
||||
|
||||
program
|
||||
.version(pkg.version)
|
||||
.command('serve', 'serve the application')
|
||||
.command('assets', 'interact with assets')
|
||||
.command('settings', 'work with the application settings')
|
||||
.command('jobs', 'work with the job queues')
|
||||
.command('users', 'work with the application auth')
|
||||
.parse(process.argv);
|
||||
|
||||
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('commander');
|
||||
const pkg = require('../package.json');
|
||||
const parseDuration = require('parse-duration');
|
||||
const Table = require('cli-table');
|
||||
const Asset = require('../models/asset');
|
||||
const mongoose = require('../mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('../util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
function listAssets() {
|
||||
Asset
|
||||
.find({})
|
||||
.sort({'created_at': 1})
|
||||
.then((asset) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'URL'
|
||||
]
|
||||
});
|
||||
|
||||
asset.forEach((asset) => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : ''
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAssets(ageString) {
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
Asset.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age
|
||||
}
|
||||
},
|
||||
{
|
||||
scraped: null
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
.then((assets) => Promise.all(assets.map(scraper.create)))
|
||||
|
||||
.then(() => {
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.version(pkg.version);
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('refresh <age>')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
.action(refreshAssets);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('../util');
|
||||
const mongoose = require('../mongoose');
|
||||
const kue = require('../kue');
|
||||
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Starts the job processor.
|
||||
*/
|
||||
function processJobs() {
|
||||
|
||||
// Start the processor.
|
||||
scraper.process();
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([
|
||||
() => scraper.shutdown()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single job.
|
||||
* @param {Object} job the job to be removed
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJob(job) {
|
||||
return new Promise((resolve, reject) => job.remove((err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(job);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the jobs passed in and returns a promise.
|
||||
* @param {Array} jobs array of jobs
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJobs(jobs) {
|
||||
return Promise.all(jobs.map(removeJob));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top n jobs with a specific state.
|
||||
* @param {String} [state='complete'] state to list jobs by
|
||||
* @param {Number} limit limit of jobs to load
|
||||
* @return {Promise}
|
||||
*/
|
||||
function rangeJobsByState(state = 'complete', limit) {
|
||||
return new Promise((resolve, reject) => {
|
||||
kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(jobs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
function cleanupJobs(options) {
|
||||
const n = 100;
|
||||
|
||||
Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
])
|
||||
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
console.log('Removed old jobs');
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('process')
|
||||
.description('starts job processing')
|
||||
.action(processJobs);
|
||||
|
||||
program
|
||||
.command('cleanup')
|
||||
.option('-s, --stuck', 'cleans up jobs that have been stuck', false)
|
||||
.description('cleans up inactive jobs')
|
||||
.action(cleanupJobs);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (process.argv.length <= 2) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/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');
|
||||
const init = require('../init');
|
||||
const scraper = require('../services/scraper');
|
||||
const mongoose = require('../mongoose');
|
||||
const util = require('../util');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
const port = normalizePort(process.env.TALK_PORT || (process.env.NODE_ENV === 'test' ? '3011' : '3000'));
|
||||
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let bind = typeof port === 'string'
|
||||
? `Pipe ${port}`
|
||||
: `Port ${port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(`${bind} is already in use`);
|
||||
break;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${ addr}`
|
||||
: `port ${ addr.port}`;
|
||||
debug(`Listening on ${ bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
function startApp() {
|
||||
init().then(() => {
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('commander');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.parse(process.argv);
|
||||
|
||||
// Start the application serving.
|
||||
startApp();
|
||||
|
||||
// Enable job processing on the thread if enabled.
|
||||
if (program.jobs) {
|
||||
|
||||
// Start the processor.
|
||||
scraper.process();
|
||||
}
|
||||
|
||||
// Define a safe shutdown function to call in the event we need to shutdown
|
||||
// because the node hooks are below which will interrupt the shutdown process.
|
||||
// Shutdown the mongoose connection, the app server, and the scraper.
|
||||
util.onshutdown([
|
||||
() => program.jobs ? scraper.shutdown() : null,
|
||||
() => mongoose.disconnect(),
|
||||
() => server.close()
|
||||
]);
|
||||
+11
-4
@@ -11,6 +11,14 @@ process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
*/
|
||||
|
||||
const program = require('commander');
|
||||
const mongoose = require('../mongoose');
|
||||
const Setting = require('../models/setting');
|
||||
const util = require('../util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
@@ -20,19 +28,17 @@ program
|
||||
.command('init')
|
||||
.description('initilizes the talk settings')
|
||||
.action(() => {
|
||||
const mongoose = require('../mongoose');
|
||||
const Setting = require('../models/setting');
|
||||
const defaults = {id: '1', moderation: 'pre'};
|
||||
|
||||
Setting
|
||||
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
|
||||
.then(() => {
|
||||
console.log('Created settings object.');
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`failed to create the settings object ${JSON.stringify(err)}`);
|
||||
throw new Error(err); // just to be safe
|
||||
util.shutdown(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,4 +47,5 @@ program.parse(process.argv);
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
+98
-60
@@ -13,14 +13,20 @@ process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
const program = require('commander');
|
||||
const pkg = require('../package.json');
|
||||
const prompt = require('prompt');
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
const util = require('../util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Regeister the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Prompts for input and registers a user based on those.
|
||||
*/
|
||||
function createUser(options) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (options.flag_mode) {
|
||||
@@ -28,6 +34,7 @@ function createUser(options) {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
displayName: options.name,
|
||||
role: options.role
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +63,11 @@ function createUser(options) {
|
||||
name: 'displayName',
|
||||
description: 'Display Name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
description: 'User Role',
|
||||
required: false
|
||||
}
|
||||
], (err, result) => {
|
||||
if (err) {
|
||||
@@ -70,15 +82,21 @@ function createUser(options) {
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim());
|
||||
})
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
mongoose.disconnect();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
return User
|
||||
.addRoleToUser(user.id, result.role.trim())
|
||||
.then(() => {
|
||||
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
|
||||
util.shutdown();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,20 +104,17 @@ function createUser(options) {
|
||||
* Deletes a user.
|
||||
*/
|
||||
function deleteUser(userID) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.findOneAndRemove({
|
||||
id: userID
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Deleted user');
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,9 +122,6 @@ function deleteUser(userID) {
|
||||
* Changes the password for a user.
|
||||
*/
|
||||
function passwd(userID) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
prompt.start();
|
||||
|
||||
prompt.get([
|
||||
@@ -128,13 +140,13 @@ function passwd(userID) {
|
||||
], (err, result) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.password !== result.confirmPassword) {
|
||||
console.error(new Error('Password mismatch'));
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,11 +154,11 @@ function passwd(userID) {
|
||||
.changePassword(userID, result.password)
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -155,9 +167,6 @@ function passwd(userID) {
|
||||
* Updates the user from the options array.
|
||||
*/
|
||||
function updateUser(userID, options) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
@@ -189,11 +198,11 @@ function updateUser(userID, options) {
|
||||
.all(updates.map((q) => q.exec()))
|
||||
.then(() => {
|
||||
console.log(`User ${userID} updated.`);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,10 +210,6 @@ function updateUser(userID, options) {
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
const Table = require('cli-table');
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.all()
|
||||
.then((users) => {
|
||||
@@ -214,6 +219,7 @@ function listUsers() {
|
||||
'Display Name',
|
||||
'Profiles',
|
||||
'Roles',
|
||||
'Status',
|
||||
'State'
|
||||
]
|
||||
});
|
||||
@@ -224,16 +230,17 @@ function listUsers() {
|
||||
user.displayName,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
user.disabled ? 'Disabled' : 'Enabled'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,18 +250,15 @@ function listUsers() {
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,18 +268,15 @@ function mergeUsers(dstUserID, srcUserID) {
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,18 +286,49 @@ function addRole(userID, role) {
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
User
|
||||
.setStatus(userID, 'banned', '')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
User
|
||||
.setStatus(userID, 'active', '')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -305,18 +337,15 @@ function removeRole(userID, role) {
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -325,18 +354,15 @@ function disableUser(userID) {
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
const User = require('../models/user');
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
mongoose.disconnect();
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
mongoose.disconnect();
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,6 +378,7 @@ program
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
@@ -393,6 +420,16 @@ program
|
||||
.description('removes a role from a given user')
|
||||
.action(removeRole);
|
||||
|
||||
program
|
||||
.command('ban <userID>')
|
||||
.description('ban a given user')
|
||||
.action(ban);
|
||||
|
||||
program
|
||||
.command('uban <userID>')
|
||||
.description('unban a given user')
|
||||
.action(unban);
|
||||
|
||||
program
|
||||
.command('disable <userID>')
|
||||
.description('disable a given user from logging in')
|
||||
@@ -408,4 +445,5 @@ program.parse(process.argv);
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:server');
|
||||
const http = require('http');
|
||||
const init = require('../init');
|
||||
const port = normalizePort(process.env.TALK_PORT || '3000');
|
||||
|
||||
let server;
|
||||
|
||||
init().then(() => {
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let bind = typeof port === 'string'
|
||||
? `Pipe ${ port}`
|
||||
: `Port ${ port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(`${bind} is already in use`);
|
||||
break;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${ addr}`
|
||||
: `port ${ addr.port}`;
|
||||
debug(`Listening on ${ bind}`);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
const redis = require('./redis');
|
||||
|
||||
const cache = module.exports = {};
|
||||
const cache = module.exports = {
|
||||
client: redis.createClient()
|
||||
};
|
||||
|
||||
/**
|
||||
* This collects a key that may either be an array or a string and creates a
|
||||
@@ -51,7 +53,7 @@ cache.wrap = (key, expiry, work) => {
|
||||
* @return {Promise}
|
||||
*/
|
||||
cache.get = (key) => new Promise((resolve, reject) => {
|
||||
redis.get(keyfunc(key), (err, reply) => {
|
||||
cache.client.get(keyfunc(key), (err, reply) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
@@ -74,6 +76,21 @@ cache.get = (key) => new Promise((resolve, reject) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This invalidates a cached entry in the cache.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @return {Promise}
|
||||
*/
|
||||
cache.invalidate = (key) => new Promise((resolve, reject) => {
|
||||
cache.client.del(keyfunc(key), (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This sets a value on the key with the expiry and then resolves once it is
|
||||
* done.
|
||||
@@ -87,7 +104,7 @@ cache.set = (key, value, expiry) => new Promise((resolve, reject) => {
|
||||
// Serialize the value as JSON.
|
||||
let reply = JSON.stringify(value);
|
||||
|
||||
redis.set(keyfunc(key), reply, 'EX', expiry, (err) => {
|
||||
cache.client.set(keyfunc(key), reply, 'EX', expiry, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
|
||||
|
||||
import ModerationQueue from 'containers/ModerationQueue';
|
||||
import CommentStream from 'containers/CommentStream';
|
||||
import Configure from 'containers/Configure';
|
||||
import ModerationQueue from 'containers/ModerationQueue/ModerationQueue';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
// Check Login
|
||||
|
||||
@@ -9,11 +9,23 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
|
||||
|
||||
export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
fetch(`${base}/auth`, getInit('GET'))
|
||||
.then(handleResp)
|
||||
coralApi('/auth')
|
||||
.then(user => {
|
||||
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
|
||||
dispatch(checkLoginSuccess(user, isAdmin));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
};
|
||||
|
||||
// LogOut Actions
|
||||
|
||||
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
|
||||
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
|
||||
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
coralApi('/auth', {method: 'DELETE'})
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Action disptacher related to comments
|
||||
*/
|
||||
@@ -16,3 +15,16 @@ export const flagComment = id => (dispatch, getState) => {
|
||||
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});
|
||||
};
|
||||
};
|
||||
|
||||
export const hideBanUserDialog = (showDialog) => {
|
||||
return dispatch => {
|
||||
dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS
|
||||
} from '../constants/community';
|
||||
|
||||
import {base, getInit, handleResp} from '../helpers/response';
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
export const fetchCommenters = (query = {}) => dispatch => {
|
||||
dispatch(requestFetchCommenters());
|
||||
fetch(`${base}/user?${qs.stringify(query)}`, getInit('GET'))
|
||||
.then(handleResp)
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
@@ -42,8 +42,15 @@ export const newPage = () => ({
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => dispatch => {
|
||||
return fetch(`${base}/user/${id}/role`, getInit('POST', {role}))
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => dispatch => {
|
||||
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
|
||||
@@ -8,38 +10,9 @@ export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
|
||||
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
const base = '/api/v1';
|
||||
|
||||
const getInit = (method, body) => {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
});
|
||||
|
||||
const init = {method, headers};
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
const handleResp = res => {
|
||||
if (res.status === 401) {
|
||||
throw new Error('Not Authorized to make this request');
|
||||
} else if (res.status > 399) {
|
||||
throw new Error('Error! Status ', res.status);
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchSettings = () => dispatch => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
fetch(`${base}/settings`, getInit('GET'))
|
||||
.then(handleResp)
|
||||
coralApi('/settings')
|
||||
.then(settings => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
@@ -55,8 +28,7 @@ export const updateSettings = settings => {
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
const settings = getState().settings.toJS().settings;
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
fetch(`${base}/settings`, getInit('PUT', settings))
|
||||
.then(handleResp)
|
||||
coralApi('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return dispatch => {
|
||||
dispatch({type: 'USER_BAN', status, userId, commentId});
|
||||
dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 280px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1, .separator h1{
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.formField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.formField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import {Dialog} from 'coral-ui';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => {
|
||||
const {userName = '', userId = '', commentId = ''} = user;
|
||||
|
||||
return (
|
||||
<Dialog className={styles.dialog} open={open} onClose={() => handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={() => handleClose()}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h3>
|
||||
{lang.t('bandialog.ban_user')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h4>
|
||||
{lang.t('bandialog.are_you_sure', userName)}
|
||||
</h4>
|
||||
<i>
|
||||
{lang.t('bandialog.note')}
|
||||
</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} full>
|
||||
{lang.t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" onClick={() => onClickBanUser(userId, commentId)} full>
|
||||
{lang.t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
@@ -1,46 +1,48 @@
|
||||
|
||||
import React from 'react';
|
||||
import timeago from 'timeago.js';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import styles from './CommentList.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import Linkify from 'react-linkify';
|
||||
import {FabButton} from 'coral-ui';
|
||||
|
||||
import {Icon} from 'react-mdl';
|
||||
import {FabButton, Button} from 'coral-ui';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
// Render a single comment for the list
|
||||
export default props => {
|
||||
const links = linkify.getMatches(props.comment.get('body'));
|
||||
const authorStatus = props.author.get('status');
|
||||
const {comment, author} = props;
|
||||
const links = linkify.getMatches(comment.get('body'));
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<i className={`material-icons ${styles.avatar}`}>person</i>
|
||||
<span>{props.comment.get('name') || lang.t('comment.anon')}</span>
|
||||
<span className={styles.created}>{timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
|
||||
{props.comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
<span>{author.get('displayName') || lang.t('comment.anon')}</span>
|
||||
<span className={styles.created}>{timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
|
||||
{comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={styles.actions}>
|
||||
{props.actions.map((action, i) => canShowAction(action, props.comment) ? (
|
||||
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
|
||||
cStyle={action}
|
||||
key={i}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
|
||||
/>
|
||||
) : null)}
|
||||
{props.actions.map((action, i) => getActionButton(action, i, props))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{authorStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<span className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
{props.comment.get('body')}
|
||||
{comment.get('body')}
|
||||
</Linkify>
|
||||
</span>
|
||||
</div>
|
||||
@@ -48,15 +50,33 @@ export default props => {
|
||||
);
|
||||
};
|
||||
|
||||
// Check if an action can be performed over a comment
|
||||
const canShowAction = (action, comment) => {
|
||||
const status = comment.get('status');
|
||||
const flagged = comment.get('flagged');
|
||||
// Get the button of the action performed over a comment if any
|
||||
const getActionButton = (action, i, props) => {
|
||||
const status = props.comment.get('status');
|
||||
const flagged = props.comment.get('flagged');
|
||||
const banned = (props.author.get('status') === 'banned');
|
||||
|
||||
if (action === 'flag' && (status || flagged === true)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
return true;
|
||||
if (action === 'ban') {
|
||||
return (
|
||||
<Button
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
cStyle='black'
|
||||
onClick={() => props.onClickShowBanDialog(props.author.get('id'), props.author.get('displayName'), props.comment.get('id'))}
|
||||
key={i} >
|
||||
{lang.t('comment.ban_user')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
|
||||
cStyle={action}
|
||||
key={i}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const linkStyles = {
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
|
||||
}
|
||||
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
@@ -133,3 +132,14 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import Comment from 'components/Comment';
|
||||
const actions = {
|
||||
'reject': {status: 'rejected', icon: 'close', key: 'r'},
|
||||
'approve': {status: 'accepted', icon: 'done', key: 't'},
|
||||
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}
|
||||
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'},
|
||||
'ban': {status: 'banned', icon: 'not interested'}
|
||||
};
|
||||
|
||||
// Renders a comment list and allow performing actions
|
||||
@@ -19,6 +20,7 @@ export default class CommentList extends React.Component {
|
||||
|
||||
this.state = {active: null};
|
||||
this.onClickAction = this.onClickAction.bind(this);
|
||||
this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this);
|
||||
}
|
||||
|
||||
// remove key handlers before leaving
|
||||
@@ -99,7 +101,8 @@ export default class CommentList extends React.Component {
|
||||
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
|
||||
// TODO: In the future this can be improved and look at the actual state to
|
||||
// resolve since the content of the list could change externally. For now it works as expected
|
||||
onClickAction (action, id) {
|
||||
onClickAction (action, id, author_id) {
|
||||
// activate the next comment
|
||||
if (id === this.state.active) {
|
||||
const {commentIds} = this.props;
|
||||
if (commentIds.last() === this.state.active) {
|
||||
@@ -108,26 +111,33 @@ export default class CommentList extends React.Component {
|
||||
this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))});
|
||||
}
|
||||
}
|
||||
this.props.onClickAction(action, id);
|
||||
this.props.onClickAction(action, id, author_id);
|
||||
}
|
||||
|
||||
onClickShowBanDialog(userId, userName, commentId) {
|
||||
this.props.onClickShowBanDialog(userId, userName, commentId);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, commentIds, comments, hideActive, key} = this.props;
|
||||
const {singleView, commentIds, comments, users, hideActive, key} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
return (
|
||||
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
|
||||
{commentIds.map((commentId, index) => (
|
||||
<Comment comment={comments.get(commentId)}
|
||||
{commentIds.map((commentId, index) => {
|
||||
const comment = comments.get(commentId);
|
||||
return <Comment comment={comment}
|
||||
author={users.get(comment.get('author_id'))}
|
||||
ref={el => { if (el && commentId === active) { this._active = el; } }}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
actions={this.props.actions}
|
||||
actionsMap={actions}
|
||||
isActive={commentId === active}
|
||||
hideActive={hideActive} />
|
||||
)).toArray()}
|
||||
hideActive={hideActive} />;
|
||||
}).toArray()}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.layout {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.layout img {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './FullLoading.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
|
||||
export const FullLoading = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Loading</h1>
|
||||
<CoralLogo />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,6 +1,5 @@
|
||||
.header {
|
||||
background: #505050;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header > div {
|
||||
@@ -14,8 +13,35 @@
|
||||
background: #232323;
|
||||
}
|
||||
|
||||
.version {
|
||||
.rightPanel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.rightPanel ul {
|
||||
list-style: none;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.rightPanel li {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.rightPanel .settings {
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #9e9e9e;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.rightPanel .settings > div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rightPanel .settings:hover {
|
||||
background: rgba(158, 158, 158, 0.69);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import React from 'react';
|
||||
import {Navigation, Header} from 'react-mdl';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
|
||||
export default () => (
|
||||
export default ({handleLogout}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo />
|
||||
<Navigation>
|
||||
<IndexLink className={styles.navLink} to="/admin" activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/community" activeClassName={styles.active}>{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure" activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
|
||||
<IndexLink className={styles.navLink} to="/admin"
|
||||
activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/community"
|
||||
activeClassName={styles.active}>{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure"
|
||||
activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
|
||||
</Navigation>
|
||||
<div className={styles.version}>
|
||||
{`v${process.env.VERSION}`}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,9 @@ import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
export const Layout = ({children}) => (
|
||||
export const Layout = ({children, ...props}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<Header />
|
||||
<Header {...props}/>
|
||||
<Drawer />
|
||||
<div className={styles.layout} >
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.logo h1 {
|
||||
color: #272727;
|
||||
font-size: 20px;
|
||||
padding: 0 30px;
|
||||
margin: 0;
|
||||
line-height: 60px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
@@ -13,6 +15,7 @@
|
||||
|
||||
.logo {
|
||||
background: #E5E5E5;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const USER_BAN_SUCESS = 'USER_BAN_SUCESS';
|
||||
@@ -4,3 +4,4 @@ export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
|
||||
export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
|
||||
|
||||
+4
-3
@@ -31,7 +31,7 @@ class CommentStream extends React.Component {
|
||||
|
||||
// The only action for now is flagging
|
||||
onClickAction (action, id) {
|
||||
if (action === 'flagged') {
|
||||
if (action === 'flag') {
|
||||
this.props.dispatch(flagComment(id));
|
||||
clearTimeout(this._snackTimeout);
|
||||
this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'});
|
||||
@@ -40,7 +40,7 @@ class CommentStream extends React.Component {
|
||||
}
|
||||
|
||||
// Render the comment box along with the CommentList
|
||||
render ({comments}, {snackbar, snackbarMsg}) {
|
||||
render ({comments, users}, {snackbar, snackbarMsg}) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CommentBox onSubmit={this.onSubmit} />
|
||||
@@ -48,6 +48,7 @@ class CommentStream extends React.Component {
|
||||
singleView={false}
|
||||
commentIds={comments.get('ids')}
|
||||
comments={comments.get('byId')}
|
||||
users={users.get('byId')}
|
||||
onClickAction={this.onClickAction}
|
||||
actions={['flag']}
|
||||
loading={comments.loading} />
|
||||
@@ -57,4 +58,4 @@ class CommentStream extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({comments}) => ({comments}))(CommentStream);
|
||||
export default connect(({comments, users}) => ({comments, users}))(CommentStream);
|
||||
@@ -1,7 +1,3 @@
|
||||
.dataTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
@@ -9,14 +5,13 @@
|
||||
.searchInput {
|
||||
display: block;
|
||||
padding-left: 40px;
|
||||
/*border: none;*/
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
/*border: 1px solid rgba(0,0,0,.12);*/
|
||||
background: white;
|
||||
}
|
||||
|
||||
.email {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ const tableHeaders = [
|
||||
title: lang.t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: lang.t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: lang.t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
@@ -30,7 +34,7 @@ const Community = ({isFetching, commenters, ...props}) => {
|
||||
const hasResults = !isFetching && !!commenters.length;
|
||||
return (
|
||||
<Grid>
|
||||
<Cell col={4}>
|
||||
<Cell col={2}>
|
||||
<form action="">
|
||||
<div className={`mdl-textfield ${styles.searchBox}`}>
|
||||
<label className="mdl-button mdl-js-button mdl-button--icon" htmlFor="commenters-search">
|
||||
@@ -49,7 +53,7 @@ const Community = ({isFetching, commenters, ...props}) => {
|
||||
</div>
|
||||
</form>
|
||||
</Cell>
|
||||
<Cell col={8}>
|
||||
<Cell col={6}>
|
||||
{ isFetching && <Loading /> }
|
||||
{ !hasResults && <NoResults /> }
|
||||
{ hasResults &&
|
||||
|
||||
@@ -40,8 +40,8 @@ class CommunityContainer extends Component {
|
||||
|
||||
this.props.dispatch(fetchCommenters({
|
||||
value: this.state.searchValue,
|
||||
field: community.get('field'),
|
||||
asc: community.get('asc'),
|
||||
field: community.field,
|
||||
asc: community.asc,
|
||||
...query
|
||||
}));
|
||||
}
|
||||
@@ -66,15 +66,19 @@ class CommunityContainer extends Component {
|
||||
return (
|
||||
<Community
|
||||
searchValue={searchValue}
|
||||
commenters={community.get('commenters')}
|
||||
isFetching={community.get('isFetching')}
|
||||
error={community.get('error')}
|
||||
totalPages={community.get('totalPages')}
|
||||
page={community.get('page')}
|
||||
commenters={community.commenters}
|
||||
isFetching={community.isFetching}
|
||||
error={community.error}
|
||||
totalPages={community.totalPages}
|
||||
page={community.page}
|
||||
{...this}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({community}) => ({community}))(CommunityContainer);
|
||||
const mapStateToProps = state => ({
|
||||
community: state.community.toJS()
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(CommunityContainer);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from './Community.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations';
|
||||
import {setRole} from '../../actions/community';
|
||||
import {setRole, setCommenterStatus} from '../../actions/community';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -19,6 +19,10 @@ class Table extends Component {
|
||||
this.props.dispatch(setRole(id, role));
|
||||
}
|
||||
|
||||
onCommenterStatusChange (id, status) {
|
||||
this.props.dispatch(setCommenterStatus(id, status));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {headers, commenters, onHeaderClickHandler} = this.props;
|
||||
|
||||
@@ -46,6 +50,14 @@ class Table extends Component {
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.status || ''}
|
||||
label={lang.t('community.status')}
|
||||
onChange={status => this.onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'active'}>{lang.t('community.active')}</Option>
|
||||
<Option value={'banned'}>{lang.t('community.banned')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
label={lang.t('community.role')}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {fetchSettings, updateSettings, saveSettingsToServer} from '../actions/settings';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
Textfield,
|
||||
Checkbox,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {activeSection: 'comments', copied: false};
|
||||
|
||||
this.copyToClipBoard = this.copyToClipBoard.bind(this);
|
||||
this.updateModeration = this.updateModeration.bind(this);
|
||||
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
|
||||
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchSettings());
|
||||
}
|
||||
|
||||
updateModeration () {
|
||||
const moderation = this.props.settings.moderation === 'pre' ? 'post' : 'pre';
|
||||
this.props.dispatch(updateSettings({moderation}));
|
||||
}
|
||||
|
||||
updateInfoBoxEnable () {
|
||||
const infoBoxEnable = !this.props.settings.infoBoxEnable;
|
||||
this.props.dispatch(updateSettings({infoBoxEnable}));
|
||||
}
|
||||
|
||||
updateInfoBoxContent (event) {
|
||||
const infoBoxContent = event.target.value;
|
||||
this.props.dispatch(updateSettings({infoBoxContent}));
|
||||
}
|
||||
|
||||
saveSettings () {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
}
|
||||
|
||||
getCommentSettings () {
|
||||
return <List>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={this.updateModeration}
|
||||
checked={this.props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
</ListItem>
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={this.updateInfoBoxEnable}
|
||||
checked={this.props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${this.props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={this.updateInfoBoxContent}
|
||||
value={this.props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
</List>;
|
||||
}
|
||||
|
||||
copyToClipBoard () {
|
||||
const copyTextarea = document.querySelector(`.${ styles.embedInput}`);
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.setState({copied: true});
|
||||
} catch (err) {
|
||||
console.error('Unable to copy', err);
|
||||
}
|
||||
}
|
||||
|
||||
getEmbed () {
|
||||
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</ListItem>
|
||||
</List>;
|
||||
}
|
||||
|
||||
changeSection (activeSection) {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
render () {
|
||||
let pageTitle = this.state.activeSection === 'comments'
|
||||
? lang.t('configure.comment-settings')
|
||||
: lang.t('configure.embed-comment-stream');
|
||||
|
||||
if (this.props.fetchingSettings) {
|
||||
pageTitle += ' - Loading...';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'comments')}
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'embed')}
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Button raised colored onClick={this.saveSettings}>
|
||||
<Icon name='save' /> {lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<h1>{pageTitle}</h1>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{
|
||||
this.state.activeSection === 'comments'
|
||||
? this.getCommentSettings()
|
||||
: this.getEmbed()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => state.settings.toJS();
|
||||
export default connect(mapStateToProps)(Configure);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
Textfield,
|
||||
Checkbox
|
||||
} from 'react-mdl';
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'pre' ? 'post' : 'pre';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
|
||||
const infoBoxEnable = !infoBox;
|
||||
updateSettings({infoBoxEnable});
|
||||
};
|
||||
|
||||
const updateInfoBoxContent = (updateSettings) => (event) => {
|
||||
const infoBoxContent = event.target.value;
|
||||
updateSettings({infoBoxContent});
|
||||
};
|
||||
|
||||
const updateClosedMessage = (updateSettings) => (event) => {
|
||||
const closedMessage = event.target.value;
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
const CommentSettings = (props) => <List>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateModeration(props.updateSettings, props.settings.moderation)}
|
||||
checked={props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
</ListItem>
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateInfoBoxEnable(props.updateSettings, props.settings.infoBoxEnable)}
|
||||
checked={props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(props.updateSettings)}
|
||||
value={props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<Textfield
|
||||
onChange={updateClosedMessage(props.updateSettings)}
|
||||
value={props.settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
</List>;
|
||||
|
||||
export default CommentSettings;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
+17
@@ -45,6 +45,10 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color:#4caf50;
|
||||
}
|
||||
|
||||
.copiedText {
|
||||
color: #008000;
|
||||
float: right;
|
||||
@@ -69,6 +73,19 @@
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
#bannedWordlist {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.bannedWordHeader {
|
||||
font-weight: bold;
|
||||
font-size:18px;
|
||||
margin-bottom:3px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {fetchSettings, updateSettings, saveSettingsToServer} from '../../actions/settings';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeSection: 'comments',
|
||||
wordlist: [],
|
||||
changed: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
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});
|
||||
}
|
||||
|
||||
changeSection = (activeSection) => () => {
|
||||
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())
|
||||
}));
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateSettings(setting));
|
||||
}
|
||||
|
||||
getSection = (section) => {
|
||||
switch(section){
|
||||
case 'comments':
|
||||
return <CommentSettings
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}/>;
|
||||
case 'embed':
|
||||
return <EmbedLink/>;
|
||||
case 'wordlist':
|
||||
return <Wordlist
|
||||
wordlist={this.state.wordlist}
|
||||
onChangeWordlist={this.onChangeWordlist}/>;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let pageTitle = this.getPageTitle(this.state.activeSection);
|
||||
const section = this.getSection(this.state.activeSection);
|
||||
|
||||
if (this.props.fetchingSettings) {
|
||||
pageTitle += ' - Loading...';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('comments')}
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('embed')}
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('wordlist')}
|
||||
icon='settings'>{lang.t('configure.wordlist')}</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
{
|
||||
this.state.changed ?
|
||||
<Button
|
||||
raised
|
||||
onClick={this.saveSettings}
|
||||
className={styles.changedSave}>
|
||||
<Icon name='check' /> {lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
: <Button
|
||||
raised
|
||||
disabled>
|
||||
{lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<h1>{pageTitle}</h1>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => state.settings.toJS();
|
||||
export default connect(mapStateToProps)(Configure);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, {Component} from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Button
|
||||
} from 'react-mdl';
|
||||
|
||||
class EmbedLink extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {copied: false};
|
||||
}
|
||||
|
||||
copyToClipBoard = () => {
|
||||
const copyTextarea = document.querySelector(`.${styles.embedInput}`);
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.setState({copied: true});
|
||||
} catch (err) {
|
||||
console.error('Unable to copy', err);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</ListItem>
|
||||
</List>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EmbedLink;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
Card
|
||||
} from 'react-mdl';
|
||||
|
||||
const Wordlist = ({wordlist, onChangeWordlist}) => <Card id={styles.bannedWordlist} shadow={2}>
|
||||
<p className={styles.bannedWordHeader}>{lang.t('configure.banned-word-header')}</p>
|
||||
<p className={styles.bannedWordText}>{lang.t('configure.banned-word-text')}</p>
|
||||
<textarea
|
||||
rows={5}
|
||||
type='text'
|
||||
className={styles.bannedWordInput}
|
||||
onChange={onChangeWordlist}
|
||||
value={wordlist}/>
|
||||
</Card>;
|
||||
|
||||
export default Wordlist;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,37 +1,31 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Layout} from '../components/ui/Layout';
|
||||
import {checkLogin} from '../actions/auth';
|
||||
import {NotFound} from '../components/NotFound';
|
||||
import {checkLogin, logout} from '../actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import {PermissionRequired} from '../components/PermissionRequired';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
componentWillMount () {
|
||||
this.props.checkLogin();
|
||||
const {checkLogin} = this.props;
|
||||
checkLogin();
|
||||
}
|
||||
render () {
|
||||
const {isAdmin, loggedIn} = this.props.auth;
|
||||
|
||||
if (!loggedIn) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
if (!isAdmin && loggedIn) {
|
||||
return <PermissionRequired />;
|
||||
}
|
||||
|
||||
return <Layout {...this.props} />;
|
||||
const {isAdmin, loggedIn, loadingUser} = this.props.auth;
|
||||
if (loadingUser) { return <FullLoading />; }
|
||||
if (!isAdmin) { return <PermissionRequired />; }
|
||||
if (isAdmin && loggedIn) { return <Layout {...this.props} />; }
|
||||
return <FullLoading />;
|
||||
}
|
||||
}
|
||||
|
||||
LayoutContainer.propTypes = {};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
+46
-14
@@ -1,16 +1,24 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import key from 'keymaster';
|
||||
|
||||
import ModerationKeysModal from 'components/ModerationKeysModal';
|
||||
import CommentList from 'components/CommentList';
|
||||
import {updateStatus} from 'actions/comments';
|
||||
import BanUserDialog from 'components/BanUserDialog';
|
||||
|
||||
import {updateStatus, showBanUserDialog, hideBanUserDialog} from 'actions/comments';
|
||||
import {banUser} from 'actions/users';
|
||||
import styles from './ModerationQueue.css';
|
||||
import key from 'keymaster';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
/*
|
||||
* Renders the moderation queue as a tabbed layout with 3 moderation
|
||||
* queues filtered by status (Untouched, Rejected and Approved)
|
||||
* queues :
|
||||
* * pending: filtered by status Untouched
|
||||
* * rejected: filtered by status Rejected
|
||||
* * flagged: with a flagged action on them
|
||||
*/
|
||||
|
||||
class ModerationQueue extends React.Component {
|
||||
@@ -45,8 +53,21 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
// Dispatch the update status action
|
||||
onCommentAction (status, id) {
|
||||
this.props.dispatch(updateStatus(status, id));
|
||||
onCommentAction (action, id) {
|
||||
// If not banning then change the status to approved or flagged as action = status
|
||||
this.props.dispatch(updateStatus(action, id));
|
||||
}
|
||||
|
||||
showBanUserDialog (userId, userName, commentId) {
|
||||
this.props.dispatch(showBanUserDialog(userId, userName, commentId));
|
||||
}
|
||||
|
||||
hideBanUserDialog () {
|
||||
this.props.dispatch(hideBanUserDialog(false));
|
||||
}
|
||||
|
||||
banUser (userId, commentId) {
|
||||
this.props.dispatch(banUser('banned', userId, commentId));
|
||||
}
|
||||
|
||||
onTabClick (activeTab) {
|
||||
@@ -55,7 +76,7 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
// Render the tabbed lists moderation queues
|
||||
render () {
|
||||
const {comments} = this.props;
|
||||
const {comments, users} = this.props;
|
||||
const {activeTab, singleView, modalOpen} = this.state;
|
||||
|
||||
return (
|
||||
@@ -75,15 +96,24 @@ class ModerationQueue extends React.Component {
|
||||
singleView={singleView}
|
||||
commentIds={
|
||||
comments.get('ids')
|
||||
.filter(id => !comments.get('byId')
|
||||
.get(id)
|
||||
.get('status'))
|
||||
.filter(id =>
|
||||
comments
|
||||
.get('byId')
|
||||
.get(id)
|
||||
.get('status') === 'premod')
|
||||
}
|
||||
comments={comments.get('byId')}
|
||||
onClickAction={(action, id) => this.onCommentAction(action, id)}
|
||||
actions={['reject', 'approve']}
|
||||
users={users.get('byId')}
|
||||
onClickAction={(action, commentId) => this.onCommentAction(action, commentId)}
|
||||
onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)}
|
||||
actions={['reject', 'approve', 'ban']}
|
||||
loading={comments.loading} />
|
||||
</div>
|
||||
<BanUserDialog
|
||||
open={comments.get('showBanUserDialog')}
|
||||
handleClose={() => this.hideBanUserDialog()}
|
||||
onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)}
|
||||
user={comments.get('banUser')}/>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
<CommentList
|
||||
isActive={activeTab === 'rejected'}
|
||||
@@ -98,6 +128,7 @@ class ModerationQueue extends React.Component {
|
||||
.get('status') === 'rejected')
|
||||
}
|
||||
comments={comments.get('byId')}
|
||||
users={users.get('byId')}
|
||||
onClickAction={(action, id) => this.onCommentAction(action, id)}
|
||||
actions={['approve']}
|
||||
loading={comments.loading} />
|
||||
@@ -111,6 +142,7 @@ class ModerationQueue extends React.Component {
|
||||
return !data.get('status') && data.get('flagged') === true;
|
||||
})}
|
||||
comments={comments.get('byId')}
|
||||
users={users.get('byId')}
|
||||
onClickAction={(action, id) => this.onCommentAction(action, id)}
|
||||
actions={['reject', 'approve']}
|
||||
loading={comments.loading} />
|
||||
@@ -123,6 +155,6 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({comments}) => ({comments}))(ModerationQueue);
|
||||
export default connect(({comments, users}) => ({comments, users}))(ModerationQueue);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,30 +0,0 @@
|
||||
export const base = '/api/v1';
|
||||
|
||||
export const getInit = (method, body) => {
|
||||
let init = {
|
||||
method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
export const handleResp = res => {
|
||||
if (res.status === 401) {
|
||||
throw new Error('Not Authorized to make this request');
|
||||
} else if (res.status > 399) {
|
||||
throw new Error('Error! Status ', res.status);
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
@@ -9,19 +9,22 @@ const initialState = Map({
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return state
|
||||
.set('loadingUser', true);
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('loadingUser', false)
|
||||
.set('user', null);
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('loadingUser', false)
|
||||
.set('isAdmin', action.isAdmin)
|
||||
.set('user', action.user);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
return initialState;
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
import * as actions from '../constants/comments';
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,13 @@ import {Map, List, fromJS} from 'immutable';
|
||||
const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List(),
|
||||
loading: false
|
||||
loading: false,
|
||||
showBanUserDialog: false,
|
||||
banUser: {
|
||||
'userName': '',
|
||||
'userId': '',
|
||||
'commentId': ''
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the comment actions
|
||||
@@ -24,10 +30,21 @@ export default (state = initialState, action) => {
|
||||
case 'COMMENT_FLAG': return flag(state, action);
|
||||
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action);
|
||||
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state);
|
||||
case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action);
|
||||
case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action);
|
||||
case actions.USER_BAN_SUCESS: return setBanUser(state, false, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
// hide or show the UI for the dialog confirming the ban
|
||||
// set the user that is going to set and the comment that is the reason
|
||||
const setBanUser = (state, showBanUser, action) => {
|
||||
const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId};
|
||||
return state.set('showBanUserDialog', showBanUser)
|
||||
.set('banUser', banUser);
|
||||
};
|
||||
|
||||
// Update a comment status
|
||||
const updateStatus = (state, action) => {
|
||||
const byId = state.get('byId');
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
SORT_UPDATE,
|
||||
SET_ROLE
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = Map({
|
||||
@@ -45,6 +46,14 @@ export default function community (state = initialState, action) {
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return state.set('commenters', commenters.map(id => id));
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.get('commenters');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return state.set('commenters', commenters.map(id => id));
|
||||
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
return state
|
||||
.set('field', action.sort.field)
|
||||
|
||||
@@ -2,6 +2,7 @@ import {combineReducers} from 'redux';
|
||||
import comments from 'reducers/comments';
|
||||
import settings from 'reducers/settings';
|
||||
import community from 'reducers/community';
|
||||
import users from 'reducers/users';
|
||||
import auth from 'reducers/auth';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
@@ -9,6 +10,6 @@ export default combineReducers({
|
||||
settings,
|
||||
comments,
|
||||
community,
|
||||
auth
|
||||
auth,
|
||||
users
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List()
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
|
||||
case 'USER_STATUS_UPDATE': return updateUserStatus(state, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Replace the comment list with a new one
|
||||
const replaceUsers = (action, state) => {
|
||||
const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
||||
return state.set('byId', users)
|
||||
.set('ids', List(users.keys()));
|
||||
};
|
||||
|
||||
// Update a user status
|
||||
const updateUserStatus = (state, action) => {
|
||||
const byId = state.get('byId');
|
||||
const data = byId.get(action.author_id).set('status', action.status.toLowerCase());
|
||||
return state.set('byId', byId.set(action.author_id, data));
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
/**
|
||||
* The adapter is a redux middleware that interecepts the actions that need
|
||||
@@ -7,9 +8,6 @@
|
||||
* for the coral but also for wordpress comments, disqus and many more.
|
||||
*/
|
||||
|
||||
// Default headers for json payloads.
|
||||
const jsonHeader = new Headers({'Content-Type': 'application/json'});
|
||||
|
||||
// Intercept redux actions and act over the ones we are interested
|
||||
export default store => next => action => {
|
||||
|
||||
@@ -17,15 +15,15 @@ export default store => next => action => {
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH':
|
||||
fetchModerationQueueComments(store);
|
||||
break;
|
||||
// case 'COMMENT_STREAM_FETCH':
|
||||
// fetchCommentStream(store);
|
||||
// break;
|
||||
case 'COMMENT_UPDATE':
|
||||
updateComment(store, action.comment);
|
||||
break;
|
||||
case 'COMMENT_CREATE':
|
||||
createComment(store, action.name, action.body);
|
||||
break;
|
||||
case 'USER_BAN':
|
||||
userStatusUpdate(store, action.status, action.userId, action.commentId);
|
||||
break;
|
||||
}
|
||||
|
||||
next(action);
|
||||
@@ -35,42 +33,61 @@ export default store => next => action => {
|
||||
const fetchModerationQueueComments = store =>
|
||||
|
||||
Promise.all([
|
||||
fetch('/api/v1/queue/comments/pending'),
|
||||
fetch('/api/v1/comments?status=rejected'),
|
||||
fetch('/api/v1/comments?action=flag')
|
||||
coralApi('/queue/comments/pending'),
|
||||
coralApi('/comments?status=rejected'),
|
||||
coralApi('/comments?action_type=flag')
|
||||
])
|
||||
.then(res => Promise.all(res.map(r => r.json())))
|
||||
.then(res => {
|
||||
res[2] = res[2].map(comment => { comment.flagged = true; return comment; });
|
||||
return res.reduce((prev, curr) => prev.concat(curr), []);
|
||||
.then(([pending, rejected, flagged]) => {
|
||||
/* Combine seperate calls into a single object */
|
||||
let all = {};
|
||||
all.comments = pending.comments
|
||||
.concat(rejected.comments)
|
||||
.concat(flagged.comments.map(comment => {
|
||||
comment.flagged = true;
|
||||
return comment;
|
||||
}));
|
||||
all.users = pending.users
|
||||
.concat(rejected.users)
|
||||
.concat(flagged.users);
|
||||
all.actions = pending.actions
|
||||
.concat(rejected.actions)
|
||||
.concat(flagged.actions);
|
||||
return all;
|
||||
})
|
||||
.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
comments: res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
|
||||
.then(all => {
|
||||
/* Post comments and users to redux store. Actions will be posted when they are needed. */
|
||||
store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
users: all.users});
|
||||
store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
comments: all.comments});
|
||||
|
||||
});
|
||||
// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
|
||||
|
||||
// Update a comment. Now to update a comment we need to send back the whole object
|
||||
|
||||
const updateComment = (store, comment) => {
|
||||
fetch(`/api/v1/comments/${comment.get('id')}/status`, {
|
||||
method: 'PUT',
|
||||
headers: jsonHeader,
|
||||
body: JSON.stringify({status: comment.get('status')})
|
||||
})
|
||||
.then(res => res.json())
|
||||
coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}})
|
||||
.then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error}));
|
||||
};
|
||||
|
||||
// Create a new comment
|
||||
const createComment = (store, name, comment) =>
|
||||
fetch('/api/v1/comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
const createComment = (store, name, comment) => {
|
||||
const body = {
|
||||
status: 'Untouched',
|
||||
body: comment,
|
||||
name: name,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
}).then(res => res.json())
|
||||
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
|
||||
};
|
||||
return coralApi('/comments', {method: 'POST', body})
|
||||
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
|
||||
};
|
||||
|
||||
// Ban a user
|
||||
const userStatusUpdate = (store, status, userId, commentId) => {
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then(res => store.dispatch({type: 'USER_BAN_SUCESS', res}))
|
||||
.catch(error => store.dispatch({type: 'USER_BAN_FAILED', error}));
|
||||
};
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
"newsroom_role": "Newsroom Role",
|
||||
"admin": "Administrator",
|
||||
"moderator": "Moderator",
|
||||
"role": "Select role..."
|
||||
"role": "Select role...",
|
||||
"no-results": "No users found with that user name or email address.",
|
||||
"status": "Status",
|
||||
"select-status": "Select status...",
|
||||
"active": "Active",
|
||||
"banned": "Banned",
|
||||
"banned-user": "Banned User",
|
||||
"loading": "Loading results"
|
||||
},
|
||||
"modqueue": {
|
||||
"pending": "pending",
|
||||
@@ -25,7 +32,9 @@
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
"anon": "Anonymous"
|
||||
"anon": "Anonymous",
|
||||
"ban_user": "Ban User",
|
||||
"banned_user": "Banned User"
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copy to Clipboard"
|
||||
@@ -37,11 +46,23 @@
|
||||
"include-text": "Include your text here.",
|
||||
"comment-settings": "Comment Settings",
|
||||
"embed-comment-stream": "Embed Comment Stream",
|
||||
"banned-word-header": "Write the bannned words list",
|
||||
"banned-word-text": "Comments which contain these words or phrases, not separated by commas and not case sensitive, will be automatically removed from the comment stream.",
|
||||
"wordlist": "Banned words list",
|
||||
"save-changes": "Save Changes",
|
||||
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
|
||||
"moderate": "Moderate",
|
||||
"configure": "Configure",
|
||||
"community": "Community"
|
||||
"community": "Community",
|
||||
"closed-comments-desc": "Write a message for closed threads",
|
||||
"closed-comments-label": "Write a message..."
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Ban User?",
|
||||
"are_you_sure": "Are you sure you would like to ban {0}?",
|
||||
"note": "Note: Banning this user will also place this comment in the Rejected queue.",
|
||||
"cancel": "Cancel",
|
||||
"yes_ban_user": "Yes, Ban User"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
@@ -51,7 +72,14 @@
|
||||
"newsroom_role": "Rol en la redacción",
|
||||
"admin": "Administrador",
|
||||
"moderator": "Moderador",
|
||||
"role": "Select role..."
|
||||
"role": "Seleccionar rol...",
|
||||
"no-results": "No se encontraron usuarios con ese nombre de usuario o correo electronico.",
|
||||
"status": "Estado",
|
||||
"select-status": "Seleccionar estado...",
|
||||
"active": "Activa",
|
||||
"banned": "Suspendido",
|
||||
"banned-user": "Usuario Suspendido",
|
||||
"loading": "Cargando resultados"
|
||||
},
|
||||
"modqueue": {
|
||||
"pending": "pendiente",
|
||||
@@ -62,7 +90,9 @@
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
"anon": "Anónimo"
|
||||
"anon": "Anónimo",
|
||||
"ban_user": "Suspender Usuario",
|
||||
"banned_user": "Usuario Suspendido"
|
||||
},
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Habilitar pre-moderación",
|
||||
@@ -71,11 +101,23 @@
|
||||
"include-text": "Incluir tu texto aqui.",
|
||||
"comment-settings": "Configuración de Comentarios",
|
||||
"embed-comment-stream": "Colocar Hilo de Comentarios",
|
||||
"wordlist": "Lista de palabras no permitidas",
|
||||
"banned-word-header": "Escribir las palabras no permitidas",
|
||||
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
|
||||
"save-changes": "Guardar Cambios",
|
||||
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
|
||||
"moderate": "Moderar",
|
||||
"configure": "Configurar",
|
||||
"community": "Comunidad"
|
||||
"community": "Comunidad",
|
||||
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
|
||||
"closed-comments-label": "Escribe un mensaje..."
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Quieres suspender el Usuario?",
|
||||
"are_you_sure": "Estas segura que quieres suspender a {props.author.displayName}?",
|
||||
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
|
||||
"cancel": "Cancelar",
|
||||
"yes_ban_user": "Si, Suspendan el usuario"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
export default ({status, onClick}) => (
|
||||
status === 'open' ? (
|
||||
<div className="close-comments-intro-wrapper">
|
||||
<p>
|
||||
This comment stream is currently open. By closing this comment stream,
|
||||
no new comments may be submitted and all previous comments will still
|
||||
be displayed.
|
||||
</p>
|
||||
<Button onClick={onClick}>Close Stream</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="close-comments-intro-wrapper">
|
||||
<p>
|
||||
This comment stream is currently closed. By opening this comment stream,
|
||||
new comments may be submitted and displayed
|
||||
</p>
|
||||
<Button onClick={onClick}>Open Stream</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.apply {
|
||||
position: absolute;
|
||||
top: 38%;
|
||||
transform: translateX(-50%);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul ul {
|
||||
padding-left: 20px
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
vertical-align: top;
|
||||
margin: 12px 12px 12px 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
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
|
||||
className={styles.apply}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
onClick={handleApply}
|
||||
>
|
||||
{lang.t('configureCommentStream.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="premod"
|
||||
onChange={handleChange}
|
||||
checked={props.premod}
|
||||
info={{
|
||||
title: lang.t('configureCommentStream.enablePremod'),
|
||||
description: lang.t('configureCommentStream.enablePremodDescription')
|
||||
}}
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="premodLinks"
|
||||
onChange={handleChange}
|
||||
checked={props.premodLinks}
|
||||
info={{
|
||||
title: lang.t('configureCommentStream.enablePremodLinks'),
|
||||
description: lang.t('configureCommentStream.enablePremodDescription')
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
|
||||
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
|
||||
class ConfigureStreamContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
premod: props.config.moderation === 'pre',
|
||||
premodLinks: false
|
||||
};
|
||||
|
||||
this.toggleStatus = this.toggleStatus.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleApply = this.handleApply.bind(this);
|
||||
}
|
||||
|
||||
handleApply () {
|
||||
const {premod, changed} = this.state;
|
||||
const newConfig = {
|
||||
moderation: premod ? 'pre' : 'post'
|
||||
};
|
||||
if (changed) {
|
||||
this.props.updateConfiguration(newConfig);
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
changed: false
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
const {name, checked} = e.target;
|
||||
this.setState({
|
||||
[name]: checked,
|
||||
changed: true
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatus () {
|
||||
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
|
||||
}
|
||||
|
||||
render () {
|
||||
const {status} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ConfigureCommentStream
|
||||
handleChange={this.handleChange}
|
||||
handleApply={this.handleApply}
|
||||
changed={this.state.changed}
|
||||
{...this.state}
|
||||
/>
|
||||
<hr />
|
||||
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
|
||||
<CloseCommentsInfo
|
||||
onClick={this.toggleStatus}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
config: state.config.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
updateStatus: status => dispatch(updateOpenStatus(status)),
|
||||
updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ConfigureStreamContainer);
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"en": {
|
||||
"configureCommentStream": {
|
||||
"apply": "Apply",
|
||||
"title": "Configure Comment Stream",
|
||||
"description": "As an admin you may customize the settings for the comment stream for this article",
|
||||
"enablePremod": "Enable Premoderation",
|
||||
"enablePremodDescription": "Moderators must approve any comment before its published.",
|
||||
"enablePremodLinks": "Pre-Moderate Comments Containing Links",
|
||||
"enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"configureCommentStream": {
|
||||
"apply": "Aplicar",
|
||||
"title": "Configurar los comentarios",
|
||||
"description": "Como Administrador puedes modificar las opciones de los comentarios en este artículo",
|
||||
"enablePremod": "Activar Pre Moderación",
|
||||
"enablePremodDescription": "Los Moderadores deben aprobar cualquier comentario antes de su publicación",
|
||||
"enablePremodLinks": "Pre-Moderar Commentarios que contienen Links",
|
||||
"enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut lobortis sollicitudin eros a ornare. Curabitur dignissim vestibulum massa non rhoncus. Cras laoreet ante vel nunc hendrerit, ac imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Pellentesque interdum nec elit sed tincidunt. Donec volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non enim nec neque congue faucibus porttitor sit amet dui.</p>
|
||||
<p>Nunc pharetra orci id diam feugiat, vitae rutrum magna efficitur. Morbi porttitor blandit lorem, et facilisis tellus luctus at. Morbi tincidunt eget nisl id placerat. Nullam consectetur quam vel mauris lacinia, non consectetur est faucibus. Duis cursus auctor nulla nec sagittis. Aenean sem erat, ultrices a hendrerit consectetur, accumsan non lorem. Integer ac neque sed magna sodales vulputate at quis neque. Praesent eget ornare lacus. Donec ultricies, dolor eget commodo faucibus, arcu velit ullamcorper tellus, in cursus tellus elit sed urna. Suspendisse in consequat magna. Duis vel ullamcorper tortor, vel cursus libero. Proin et nisi luctus ligula faucibus luctus. Morbi pulvinar, justo ac feugiat elementum, libero tellus congue justo, pharetra ultrices felis felis id leo. Integer mattis quam tempus libero porta, ac pretium ligula elementum.</p>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
|
||||
<script type='text/javascript' src='/client/js/lib/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', 'index.html', {title: 'comments'});
|
||||
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})</script>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import Pym from 'pym.js';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {
|
||||
itemActions,
|
||||
Notification,
|
||||
notificationActions,
|
||||
authActions
|
||||
} from '../../coral-framework';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
import InfoBox from '../../coral-plugin-infobox/InfoBox';
|
||||
import Content from '../../coral-plugin-commentcontent/CommentContent';
|
||||
@@ -13,45 +16,41 @@ import PubDate from '../../coral-plugin-pubdate/PubDate';
|
||||
import Count from '../../coral-plugin-comment-count/CommentCount';
|
||||
import AuthorName from '../../coral-plugin-author-name/AuthorName';
|
||||
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
|
||||
import Pym from 'pym.js';
|
||||
import FlagButton from '../../coral-plugin-flags/FlagButton';
|
||||
import FlagComment from '../../coral-plugin-flags/FlagComment';
|
||||
import LikeButton from '../../coral-plugin-likes/LikeButton';
|
||||
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import UserBox from '../../coral-sign-in/components/UserBox';
|
||||
import CommentHistory from '../../coral-plugin-history/CommentHistory';
|
||||
|
||||
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
|
||||
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
|
||||
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
|
||||
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
|
||||
|
||||
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
|
||||
|
||||
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {logout} = authActions;
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
config: state.config.toJS(),
|
||||
items: state.items.toJS(),
|
||||
notification: state.notification.toJS(),
|
||||
auth: state.auth.toJS()
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
|
||||
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
|
||||
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
|
||||
getStream: (rootId) => dispatch(getStream(rootId)),
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
|
||||
deleteAction: (item, action, user, itemType) => {
|
||||
return dispatch(deleteAction(item, action, user, itemType));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront, itemType) =>
|
||||
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
|
||||
logout: () => dispatch(logout())
|
||||
});
|
||||
const {logout, showSignInDialog} = authActions;
|
||||
|
||||
class CommentStream extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTab: 0
|
||||
};
|
||||
|
||||
this.changeTab = this.changeTab.bind(this);
|
||||
}
|
||||
|
||||
changeTab (tab) {
|
||||
this.setState({
|
||||
activeTab: tab
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.object.isRequired,
|
||||
addItem: PropTypes.func.isRequired,
|
||||
@@ -60,170 +59,266 @@ class CommentStream extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
// Using recommended Pym init code which violates .eslint standards
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl);
|
||||
this.props.getStream(path && path[1] || window.location);
|
||||
this.pym = new Pym.Child({polling: 100});
|
||||
|
||||
const path = this.pym.parentUrl.split('#')[0];
|
||||
|
||||
this.props.getStream(path || window.location);
|
||||
this.path = path;
|
||||
|
||||
this.pym.sendMessage('childReady');
|
||||
|
||||
this.pym.onMessage('DOMContentLoaded', hash => {
|
||||
const commentId = hash.replace('#', 'c_');
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (document.getElementById(commentId)) {
|
||||
window.clearInterval(interval);
|
||||
this.pym.scrollParentToChildEl(commentId);
|
||||
}
|
||||
|
||||
if (++count > 100) { // ~10 seconds
|
||||
// give up waiting for the comments to load.
|
||||
// it would be weird for the page to jump after that long.
|
||||
window.clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
if (Object.keys(this.props.items).length === 0) {
|
||||
// Loading mock asset
|
||||
this.props.postItem({
|
||||
comments: [],
|
||||
url: 'http://coralproject.net'
|
||||
}, 'asset', 'assetTest');
|
||||
|
||||
// Loading mock user
|
||||
//this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
// .then((id) => {
|
||||
// this.props.setLoggedInUser(id);
|
||||
// });
|
||||
}
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
|
||||
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
|
||||
const {actions, users, comments} = this.props.items;
|
||||
const {loggedIn, user, showSignInDialog} = this.props.auth;
|
||||
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
|
||||
<CommentHistory />
|
||||
const {status, moderation, closedMessage} = this.props.config;
|
||||
const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const {activeTab} = this.state;
|
||||
const banned = (this.props.userData.status === 'banned');
|
||||
|
||||
const expandForLogin = showSignInDialog ? {
|
||||
minHeight: document.body.scrollHeight + 150
|
||||
} : {};
|
||||
return <div style={expandForLogin}>
|
||||
{
|
||||
rootItem
|
||||
? <div>
|
||||
<div id="commentBox">
|
||||
<InfoBox
|
||||
content={this.props.config.infoBoxContent}
|
||||
enable={this.props.config.infoBoxEnable}/>
|
||||
<Count
|
||||
id={rootItemId}
|
||||
items={this.props.items}/>
|
||||
? <div className="commentStream">
|
||||
<TabBar onChange={this.changeTab} activeTab={activeTab}>
|
||||
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
|
||||
<Tab>Settings</Tab>
|
||||
<Tab>Configure Stream</Tab>
|
||||
</TabBar>
|
||||
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
premod={this.props.config.moderation}
|
||||
reply={false}
|
||||
author={user}
|
||||
/>
|
||||
{!loggedIn && <SignInContainer />}
|
||||
</div>
|
||||
{
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
const comment = comments[commentId];
|
||||
return <div className="comment" key={commentId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName author={users[comment.author_id]}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}
|
||||
showReply={comment.showReply}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
like={actions[comment.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
flag={actions[comment.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
comment_id={commentId}
|
||||
asset_id={comment.asset_id}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
premod={this.props.config.moderation}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.children &&
|
||||
comment.children.map((replyId) => {
|
||||
let reply = this.props.items.comments[replyId];
|
||||
return <div className="reply" key={replyId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName author={users[reply.author_id]}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content body={reply.body}/>
|
||||
<div className="replyActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={replyId}
|
||||
showReply={reply.showReply}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
like={this.props.items.actions[reply.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
</div>
|
||||
<div className="replyActionsRight">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
flag={this.props.items.actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
comment_id={reply.parent_id}
|
||||
asset_id={rootItemId}
|
||||
/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
child_id={replyId}
|
||||
premod={this.props.config.moderation}
|
||||
showReply={reply.showReply}/>
|
||||
</div>;
|
||||
})
|
||||
<TabContent show={activeTab === 0}>
|
||||
{
|
||||
status === 'open'
|
||||
? <div id="commentBox">
|
||||
<InfoBox
|
||||
content={this.props.config.infoBoxContent}
|
||||
enable={this.props.config.infoBoxEnable}
|
||||
/>
|
||||
<RestrictedContent restricted={banned} restrictedComp={<SuspendedAccount />}>
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
premod={moderation}
|
||||
reply={false}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}
|
||||
author={user}
|
||||
/>
|
||||
</RestrictedContent>
|
||||
</div>
|
||||
: <p>{closedMessage}</p>
|
||||
}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
{!loggedIn && <SignInContainer offset={signInOffset} />}
|
||||
{
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
const comment = comments[commentId];
|
||||
return <div className="comment" key={commentId} id={`c_${commentId}`}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName
|
||||
author={users[comment.author_id]}
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
author_id={comment.author_id}
|
||||
postAction={this.props.postAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}
|
||||
currentUser={this.props.auth.user}
|
||||
showReply={comment.showReply}
|
||||
banned={banned}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
like={actions[comment.like]}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}/>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<FlagComment
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
author_id={comment.author_id}
|
||||
flag={actions[comment.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
commentId={commentId}
|
||||
articleURL={this.path}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
premod={moderation}
|
||||
currentUser={user}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.children &&
|
||||
comment.children.map((replyId) => {
|
||||
let reply = this.props.items.comments[replyId];
|
||||
return <div className="reply" key={replyId} id={`c_${replyId}`}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName author={users[reply.author_id]}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content body={reply.body}/>
|
||||
<div className="replyActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={replyId}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}
|
||||
showReply={reply.showReply}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
like={this.props.items.actions[reply.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}/>
|
||||
</div>
|
||||
<div className="replyActionsRight">
|
||||
<FlagComment
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
author_id={comment.author_id}
|
||||
flag={actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
commentId={reply.parent_id}
|
||||
articleURL={this.path}
|
||||
/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
child_id={replyId}
|
||||
premod={moderation}
|
||||
banned={banned}
|
||||
currentUser={user}
|
||||
showReply={reply.showReply}/>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
<SettingsContainer
|
||||
loggedIn={loggedIn}
|
||||
userData={this.props.userData}
|
||||
showSignInDialog={this.props.handleSignInDialog}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 2}>
|
||||
<RestrictedContent restricted={!loggedIn}>
|
||||
<ConfigureStreamContainer
|
||||
status={status}
|
||||
onClick={this.toggleStatus}
|
||||
/>
|
||||
</RestrictedContent>
|
||||
</TabContent>
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}/>
|
||||
notification={this.props.notification}
|
||||
/>
|
||||
</div>
|
||||
: 'Loading'
|
||||
:
|
||||
<Spinner/>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
config: state.config.toJS(),
|
||||
items: state.items.toJS(),
|
||||
notification: state.notification.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
userData: state.user.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
addItem: (item, item_id) => dispatch(addItem(item, item_id)),
|
||||
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
|
||||
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
|
||||
getStream: (rootId) => dispatch(getStream(rootId)),
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)),
|
||||
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
|
||||
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
|
||||
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
|
||||
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
|
||||
logout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
|
||||
|
||||
@@ -2,9 +2,7 @@ import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import CommentStream from './CommentStream';
|
||||
import {Provider} from 'react-redux';
|
||||
import {fetchConfig, store} from '../../coral-framework';
|
||||
|
||||
store.dispatch(fetchConfig());
|
||||
import {store} from '../../coral-framework';
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
|
||||
@@ -8,7 +8,7 @@ body {
|
||||
}
|
||||
|
||||
.expandForSignin {
|
||||
min-height: 550px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -56,12 +56,14 @@ hr {
|
||||
|
||||
/* Info Box Styles */
|
||||
.coral-plugin-infobox-info {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
@@ -71,6 +73,11 @@ hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.commentStream {
|
||||
/* prevent absolutely positioned final permalink popover from being clipped */
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
/* Comment Box Styles */
|
||||
.coral-plugin-commentbox-container {
|
||||
display: flex;
|
||||
@@ -79,6 +86,7 @@ hr {
|
||||
.coral-plugin-commentbox-textarea {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.coral-plugin-commentbox-button-container {
|
||||
@@ -106,6 +114,7 @@ hr {
|
||||
/* Comment styles */
|
||||
.comment {
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coral-plugin-commentcontent-text {
|
||||
@@ -118,6 +127,10 @@ hr {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.coral-plugin-author-name-bio-flag {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/* Reply styles */
|
||||
|
||||
@@ -139,9 +152,8 @@ hr {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.commentActionsLeft .material-icons,.commentActionsRight .material-icons,
|
||||
.replyActionsLeft .material-icons, .replyActionsRight .material-icons {
|
||||
font-size: 12px;
|
||||
.material-icons {
|
||||
font-size: 12px !important;
|
||||
margin-left: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -154,12 +166,124 @@ hr {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
/* Comment count styles */
|
||||
.coral-plugin-comment-count-text {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.coral-plugin-pubdate-text {
|
||||
color: #CCC;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.coral-plugin-permalinks-container {
|
||||
/*position: relative;*/
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.coral-plugin-permalinks-popover {
|
||||
display: none;
|
||||
background-color: white;
|
||||
border: 1px solid black;
|
||||
width: calc(100% - 15px);
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.coral-plugin-permalinks-popover.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.coral-plugin-permalinks-copy-field {
|
||||
display: block;
|
||||
width: calc(100% - 5px);
|
||||
}
|
||||
|
||||
.coral-plugin-permalinks-copied-text {
|
||||
float: right;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
/* Flag Styles */
|
||||
|
||||
.coral-plugin-flags-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup span {
|
||||
min-width: 280px;
|
||||
bottom: 36px;
|
||||
left: -190px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup-form {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup-header {
|
||||
font-weight: bolder;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup-radio {
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup-radio-label {
|
||||
margin:5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup-counter {
|
||||
float: left;
|
||||
margin-top: 21px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-popup-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.coral-plugin-flags-other-text {
|
||||
margin-left: 20px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
/* Close comments */
|
||||
|
||||
.close-comments-intro-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close-comments-intro-wrapper button {
|
||||
width: 300px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.close-comments-intro-wrapper button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.close-comments-message {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.close-comments-confirm-wrapper {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.close-comments-alert {
|
||||
background-color: #d65344;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.close-comments-alert i.material-icons {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
import coralApi, {base} from '../helpers/response';
|
||||
import {addItem} from './items';
|
||||
|
||||
// Dialog Actions
|
||||
export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG});
|
||||
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
|
||||
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
|
||||
export const changeView = view => dispatch =>
|
||||
@@ -25,8 +25,7 @@ const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
|
||||
export const fetchSignIn = (formData) => dispatch => {
|
||||
dispatch(signInRequest());
|
||||
fetch(`${base}/auth/local`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(signInSuccess(user));
|
||||
@@ -74,8 +73,7 @@ const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
|
||||
|
||||
export const fetchSignUp = formData => dispatch => {
|
||||
dispatch(signUpRequest());
|
||||
fetch(`${base}/user`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
coralApi('/user', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
setTimeout(() =>{
|
||||
@@ -93,8 +91,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU
|
||||
|
||||
export const fetchForgotPassword = email => dispatch => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
fetch(`${base}/user/request-password-reset`, getInit('POST', {email}))
|
||||
.then(handleResp)
|
||||
coralApi('/users/request-password-reset', {method: 'POST', body: {email}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
@@ -107,8 +104,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
fetch(`${base}/auth`, getInit('DELETE'))
|
||||
.then(handleResp)
|
||||
coralApi('/auth', {method: 'DELETE'})
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
@@ -126,8 +122,13 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
|
||||
|
||||
export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
fetch(`${base}/auth`, getInit('GET'))
|
||||
.then(handleResp)
|
||||
.then(user => dispatch(checkLoginSuccess(user)))
|
||||
coralApi('/auth')
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
throw new Error('not logged in');
|
||||
}
|
||||
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
};
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
import {fromJS} from 'immutable';
|
||||
import coralApi from '../helpers/response';
|
||||
import * as actions from '../constants/config';
|
||||
import {addNotification} from '../actions/notification';
|
||||
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export const FETCH_CONFIG_REQUEST = 'FETCH_CONFIG_REQUEST';
|
||||
export const FETCH_CONFIG_FAILED = 'FETCH_CONFIG_FAILED';
|
||||
export const FETCH_CONFIG_SUCCESS = 'FETCH_CONFIG_SUCCESS';
|
||||
export const updateOpenStatus = status => (dispatch, getState) => {
|
||||
const assetId = getState().items.get('assets')
|
||||
.keySeq()
|
||||
.toArray()[0];
|
||||
return coralApi(`/asset/${assetId}/status?status=${status}`, {method: 'PUT'})
|
||||
.then(() => dispatch({type: status === 'open' ? actions.OPEN_COMMENTS : actions.CLOSE_COMMENTS}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
const updateConfigRequest = () => ({type: actions.UPDATE_CONFIG_REQUEST});
|
||||
const updateConfigSuccess = config => ({type: actions.UPDATE_CONFIG_SUCCESS, config});
|
||||
const updateConfigFailure = () => ({type: actions.UPDATE_CONFIG_FAILURE});
|
||||
|
||||
export function fetchConfig () {
|
||||
return (dispatch) => {
|
||||
|
||||
dispatch({type: FETCH_CONFIG_REQUEST});
|
||||
export const updateConfiguration = newConfig => (dispatch, getState) => {
|
||||
const assetId = getState().items.get('assets')
|
||||
.keySeq()
|
||||
.toArray()[0];
|
||||
|
||||
return fetch('/api/v1/settings')
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
.then((json) => {
|
||||
return dispatch({type: FETCH_CONFIG_SUCCESS, config: fromJS(json)});
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({type: FETCH_CONFIG_FAILED, error});
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
dispatch(updateConfigRequest());
|
||||
coralApi(`/asset/${assetId}/settings`, {method: 'PUT', body: newConfig})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', lang.t('successUpdateSettings')));
|
||||
dispatch(updateConfigSuccess(newConfig));
|
||||
})
|
||||
.catch(error => dispatch(updateConfigFailure(error)));
|
||||
};
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import coralApi from '../helpers/response';
|
||||
import {fromJS} from 'immutable';
|
||||
import {UPDATE_CONFIG} from '../constants/config';
|
||||
|
||||
/* Item Actions */
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const REQUEST_COMMENTS_BY_USER = 'REQUEST_COMMENTS_BY_USER';
|
||||
export const RECEIVE_COMMENTS_BY_USER = 'RECEIVE_COMMENTS_BY_USER';
|
||||
export const FAILURE_COMMENTS_BY_USER = 'FAILURE_COMMENTS_BY_USER';
|
||||
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
|
||||
|
||||
const getInit = (method, body) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
/* Item Actions */
|
||||
|
||||
const init = {method, headers};
|
||||
if (body) {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
const responseHandler = response => {
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
|
||||
};
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
@@ -67,6 +48,7 @@ export const addItem = (item, item_type) => {
|
||||
* id - the id of the item to be posted
|
||||
* property - the property to be updated
|
||||
* value - the value that the property should be set to
|
||||
* item_type - the type of the item being updated (users, comments, etc)
|
||||
*
|
||||
*/
|
||||
export const updateItem = (id, property, value, item_type) => {
|
||||
@@ -79,6 +61,18 @@ export const updateItem = (id, property, value, item_type) => {
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Appends data to an array in an item in the local store without posting it to the server
|
||||
* Useful for adding a recently posted reply to a comment, etc.
|
||||
*
|
||||
* @params
|
||||
* id - the id of the item to be posted
|
||||
* property - the property to be updated (should be an array)
|
||||
* value - the value that should be added to the array
|
||||
* add_to_front - boolean that defines whether value is added at the beginning (unshift) or end (push)
|
||||
* item_type - the type of the item being updated (users, comments, etc)
|
||||
*
|
||||
*/
|
||||
export const appendItemArray = (id, property, value, add_to_front, item_type) => {
|
||||
return {
|
||||
type: APPEND_ITEM_ARRAY,
|
||||
@@ -131,8 +125,7 @@ export const fetchCommentsByUserId = userId => {
|
||||
*/
|
||||
export function getStream (assetUrl) {
|
||||
return (dispatch) => {
|
||||
return fetch(`/api/v1/stream?asset_url=${encodeURIComponent(assetUrl)}`)
|
||||
.then(responseHandler)
|
||||
return coralApi(`/stream?asset_url=${encodeURIComponent(assetUrl)}`)
|
||||
.then((json) => {
|
||||
|
||||
/* Add items to the store */
|
||||
@@ -142,6 +135,8 @@ export function getStream (assetUrl) {
|
||||
action.id = `${action.action_type}_${action.item_id}`;
|
||||
dispatch(addItem(action, 'actions'));
|
||||
});
|
||||
} else if (type === 'settings') {
|
||||
dispatch({type: UPDATE_CONFIG, config: fromJS(json[type])});
|
||||
} else {
|
||||
json[type].forEach(item => {
|
||||
dispatch(addItem(item, type));
|
||||
@@ -201,8 +196,7 @@ export function getStream (assetUrl) {
|
||||
|
||||
export function getItemsArray (ids) {
|
||||
return (dispatch) => {
|
||||
return fetch(`/v1/item/${ids}`, getInit('GET'))
|
||||
.then(responseHandler)
|
||||
return coralApi(`/item/${ids}`)
|
||||
.then((json) => {
|
||||
for (let i = 0; i < json.items.length; i++) {
|
||||
dispatch(addItem(json.items[i]));
|
||||
@@ -231,11 +225,10 @@ export function postItem (item, type, id) {
|
||||
if (id) {
|
||||
item.id = id;
|
||||
}
|
||||
return fetch(`/api/v1/${type}`, getInit('POST', item))
|
||||
.then(responseHandler)
|
||||
return coralApi(`/${type}`, {method: 'POST', body: item})
|
||||
.then((json) => {
|
||||
dispatch(addItem({...item, id:json.id}, type));
|
||||
return json.id;
|
||||
return json;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -255,15 +248,9 @@ export function postItem (item, type, id) {
|
||||
*
|
||||
*/
|
||||
|
||||
export function postAction (item_id, action_type, user_id, item_type) {
|
||||
export function postAction (item_id, item_type, action) {
|
||||
return () => {
|
||||
const action = {
|
||||
action_type,
|
||||
user_id
|
||||
};
|
||||
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('POST', action))
|
||||
.then(responseHandler);
|
||||
return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -284,7 +271,6 @@ export function postAction (item_id, action_type, user_id, item_type) {
|
||||
|
||||
export function deleteAction (action_id) {
|
||||
return () => {
|
||||
return fetch(`/api/v1/actions/${action_id}`, {method: 'DELETE'})
|
||||
.then(responseHandler);
|
||||
return coralApi(`/actions/${action_id}`, {method: 'DELETE'});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as actions from '../constants/user';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import coralApi from '../helpers/response';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const saveBioRequest = () => ({type: actions.SAVE_BIO_REQUEST});
|
||||
const saveBioSuccess = settings => ({type: actions.SAVE_BIO_SUCCESS, settings});
|
||||
const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
|
||||
|
||||
export const saveBio = (user_id, formData) => dispatch => {
|
||||
dispatch(saveBioRequest());
|
||||
coralApi(`/users/${user_id}/bio`, {method: 'PUT', body: formData})
|
||||
.then(({settings}) => {
|
||||
dispatch(addNotification('success', lang.t('successBioUpdate')));
|
||||
dispatch(saveBioSuccess(settings));
|
||||
})
|
||||
.catch(error => dispatch(saveBioFailure(error)));
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
.message {
|
||||
background: #D8D8D8;
|
||||
padding: 25px;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import styles from './RestrictedContent.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-framework/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default ({children, restricted, message = lang.t('contentNotAvailable'), restrictedComp}) => {
|
||||
if (restricted) {
|
||||
return restrictedComp ? restrictedComp : messageBox(message);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const messageBox = (message) => <div className={styles.message}>{message}</div>;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-framework/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
import styles from './RestrictedContent.css';
|
||||
|
||||
export default () => (
|
||||
<div className={styles.message}>
|
||||
<span>{lang.t('suspendedAccountMsg')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
|
||||
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
|
||||
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
|
||||
|
||||
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
|
||||
|
||||
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
|
||||
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
@@ -0,0 +1,3 @@
|
||||
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
|
||||
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
|
||||
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
|
||||
@@ -1,23 +1,25 @@
|
||||
export const base = '/api/v1';
|
||||
|
||||
export const getInit = (method, body) => {
|
||||
let init = {
|
||||
method,
|
||||
headers: new Headers({
|
||||
const buildOptions = (inputOptions = {}) => {
|
||||
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}),
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
const options = Object.assign({}, defaultOptions, inputOptions);
|
||||
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
if (options.method.toLowerCase() !== 'get') {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
return init;
|
||||
return options;
|
||||
};
|
||||
|
||||
export const handleResp = res => {
|
||||
const handleResp = res => {
|
||||
if (res.status === 401) {
|
||||
throw new Error('Not Authorized to make this request');
|
||||
} else if (res.status > 399) {
|
||||
@@ -28,3 +30,7 @@ export const handleResp = res => {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
export default (url, options) => {
|
||||
return fetch(`${base}${url}`, buildOptions(options)).then(handleResp);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Notification from './modules/notification/Notification';
|
||||
import store from './store';
|
||||
import {fetchConfig} from './actions/config';
|
||||
import * as itemActions from './actions/items';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import * as notificationActions from './actions/notification';
|
||||
import * as authActions from './actions/auth';
|
||||
import * as configActions from './actions/config';
|
||||
|
||||
export {
|
||||
Notification,
|
||||
store,
|
||||
fetchConfig,
|
||||
itemActions,
|
||||
I18n,
|
||||
notificationActions,
|
||||
authActions
|
||||
authActions,
|
||||
configActions
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import timeago from 'timeago.js';
|
||||
import esTA from '../../../../node_modules/timeago.js/locales/es';
|
||||
import has from 'lodash/has';
|
||||
import get from 'lodash/get';
|
||||
|
||||
/**
|
||||
* Default locales, this should be overriden by config file
|
||||
@@ -46,21 +48,18 @@ class i18n {
|
||||
* it takes a string with the translation key and returns
|
||||
* the translation value or the key itself if not found
|
||||
* it works with nested translations (my.page.title)
|
||||
*
|
||||
* any extra parameters are optional and replace a variable marked by {0}, {1}, etc in the translation.
|
||||
*/
|
||||
|
||||
this.t = (key) => {
|
||||
const arr = key.split('.');
|
||||
let translation = this.translations;
|
||||
try {
|
||||
for (let i = 0; i < arr.length; i++) {translation = translation[arr[i]];}
|
||||
} catch (error) {
|
||||
console.warn(`${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
const val = String(translation);
|
||||
if (val) {
|
||||
return val;
|
||||
this.t = (key, ...replacements) => {
|
||||
if (has(this.translations, key)) {
|
||||
let translation = get(this.translations, key);
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${key} language key not set`);
|
||||
return key;
|
||||
|
||||
@@ -13,11 +13,17 @@ const initialState = Map({
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
const purge = user => {
|
||||
const {settings, profiles, ...userData} = user; // eslint-disable-line
|
||||
return userData;
|
||||
};
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SIGNIN_DIALOG :
|
||||
return state
|
||||
.set('showSignInDialog', true);
|
||||
.set('showSignInDialog', true)
|
||||
.set('signInOffset', action.offset);
|
||||
case actions.HIDE_SIGNIN_DIALOG :
|
||||
return state.merge(Map({
|
||||
isLoading: false,
|
||||
@@ -44,11 +50,11 @@ export default function auth (state = initialState, action) {
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('user', action.user);
|
||||
.set('user', purge(action.user));
|
||||
case actions.FETCH_SIGNIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('user', action.user);
|
||||
.set('user', purge(action.user));
|
||||
case actions.FETCH_SIGNIN_FAILURE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
@@ -56,7 +62,7 @@ export default function auth (state = initialState, action) {
|
||||
.set('user', null);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return state
|
||||
.set('user', action.user)
|
||||
.set('user', purge(action.user))
|
||||
.set('loggedIn', true);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
|
||||
return state
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
/* @flow */
|
||||
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../actions/config';
|
||||
import * as actions from '../constants/config';
|
||||
|
||||
const initialState = Map({
|
||||
features: Map({})
|
||||
features: Map({}),
|
||||
status: 'open',
|
||||
moderation: null
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch(action.type) {
|
||||
case actions.FETCH_CONFIG_REQUEST:
|
||||
return state.set('loading', true);
|
||||
|
||||
case actions.FETCH_CONFIG_FAILED:
|
||||
return state.set('loading', false);
|
||||
|
||||
// Override config if worked
|
||||
case actions.FETCH_CONFIG_SUCCESS:
|
||||
return action.config.set('loading', false);
|
||||
|
||||
case actions.UPDATE_CONFIG:
|
||||
return state
|
||||
.merge(Map(action.config));
|
||||
case actions.UPDATE_CONFIG_SUCCESS:
|
||||
return state
|
||||
.merge(Map(action.config));
|
||||
case actions.OPEN_COMMENTS:
|
||||
return state
|
||||
.set('status', 'open');
|
||||
case actions.CLOSE_COMMENTS:
|
||||
return state
|
||||
.set('status', 'closed');
|
||||
case actions.ADD_ITEM:
|
||||
return action.item_type === 'assets' ? state.set('status', action.item.status) : state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import config from './config';
|
||||
import items from './items';
|
||||
import notification from './notification';
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
|
||||
/**
|
||||
* Expose the combined main reducer
|
||||
@@ -14,5 +15,6 @@ export default combineReducers({
|
||||
config,
|
||||
items,
|
||||
notification,
|
||||
auth
|
||||
auth,
|
||||
user
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as authActions from '../constants/auth';
|
||||
import * as actions from '../constants/user';
|
||||
|
||||
const initialState = Map({
|
||||
displayName: '',
|
||||
profiles: [],
|
||||
settings: {}
|
||||
});
|
||||
|
||||
const purge = user => {
|
||||
const {_id, created_at, updated_at, __v, roles, ...userData} = user; // eslint-disable-line
|
||||
return userData;
|
||||
};
|
||||
|
||||
export default function user (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case authActions.CHECK_LOGIN_SUCCESS:
|
||||
return state.merge(Map(purge(action.user)));
|
||||
case authActions.CHECK_LOGIN_FAILURE:
|
||||
return initialState;
|
||||
case authActions.FETCH_SIGNIN_SUCCESS:
|
||||
return state.merge(Map(purge(action.user)));
|
||||
case authActions.FETCH_SIGNIN_FAILURE:
|
||||
return initialState;
|
||||
case authActions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return state.merge(Map(purge(action.user)));
|
||||
case authActions.FETCH_SIGNIN_FACEBOOK_FAILURE:
|
||||
return initialState;
|
||||
case actions.SAVE_BIO_SUCCESS:
|
||||
return state
|
||||
.set('settings', action.settings);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"en": {
|
||||
"successUpdateSettings": "The changes you have made have been applied to the comment stream on this article",
|
||||
"successBioUpdate": "Your Bio has been updated",
|
||||
"contentNotAvailable": "This content is not available",
|
||||
"suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
|
||||
"error": {
|
||||
"email": "Not a valid E-Mail",
|
||||
"password": "Password must be at least 8 characters",
|
||||
@@ -10,6 +14,10 @@
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"successUpdateSettings": "La configuración de este articulo fue actualizada",
|
||||
"successBioUpdate": "Tu bio fue actualizada",
|
||||
"contentNotAvailable": "El contenido no se encuentra disponible",
|
||||
"suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
|
||||
"error": {
|
||||
"email": "No es un email válido",
|
||||
"password": "La contraseña debe tener por lo menos 8 caracteres",
|
||||
@@ -19,4 +27,4 @@
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
import React from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import {Tooltip} from 'coral-ui';
|
||||
import FlagBio from '../coral-plugin-flags/FlagBio';
|
||||
const packagename = 'coral-plugin-author-name';
|
||||
|
||||
const AuthorName = ({author}) =>
|
||||
<div className={`${packagename}-text`}>
|
||||
{author && author.displayName}
|
||||
</div>;
|
||||
export default class AuthorName extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
export default AuthorName;
|
||||
this.state = {
|
||||
showTooltip: false
|
||||
};
|
||||
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
handleMouseOver () {
|
||||
this.setState({
|
||||
showTooltip: true
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseLeave () {
|
||||
this.setState({
|
||||
showTooltip: false
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {author} = this.props;
|
||||
const {showTooltip} = this.state;
|
||||
return (
|
||||
<div
|
||||
className={`${packagename}-text`}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
{author && author.displayName}
|
||||
{ showTooltip && <Tooltip>
|
||||
<div className={`${packagename}-bio`}>
|
||||
{author.settings.bio}
|
||||
</div>
|
||||
<div className={`${packagename}-bio-flag`}>
|
||||
<FlagBio {...this.props}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
const name = 'coral-plugin-commentbox';
|
||||
|
||||
@@ -38,16 +39,22 @@ class CommentBox extends Component {
|
||||
related = 'comments';
|
||||
parent_type = 'assets';
|
||||
}
|
||||
updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
if (child_id || parent_id) {
|
||||
updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
}
|
||||
postItem(comment, 'comments')
|
||||
.then((comment_id) => {
|
||||
if (premod === 'pre') {
|
||||
addNotification('success', lang.t('comment-post-notif-premod'));
|
||||
} else {
|
||||
appendItemArray(parent_id || id, related, comment_id, !parent_id, parent_type);
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}
|
||||
})
|
||||
.then((postedComment) => {
|
||||
const commentId = postedComment.id;
|
||||
const status = postedComment.status;
|
||||
if (status[0] && status[0].type === 'rejected') {
|
||||
addNotification('error', lang.t('comment-post-banned-word'));
|
||||
} else if (premod === 'pre') {
|
||||
addNotification('success', lang.t('comment-post-notif-premod'));
|
||||
} else {
|
||||
appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
this.setState({body: ''});
|
||||
}
|
||||
@@ -75,12 +82,12 @@ class CommentBox extends Component {
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
{ author && (
|
||||
<button
|
||||
<Button
|
||||
cStyle='darkGrey'
|
||||
className={`${name}-button`}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
"comment": "Comment",
|
||||
"name": "Name",
|
||||
"comment-post-notif": "Your comment has been posted.",
|
||||
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly."
|
||||
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
|
||||
"comment-post-banned-word": "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team."
|
||||
},
|
||||
"es": {
|
||||
"post": "Publicar",
|
||||
"reply": "Respuesta",
|
||||
"comment": "Comentario",
|
||||
"name": "Nombre",
|
||||
"comment-post-notif": "¡traduceme!",
|
||||
"comment-post-notif-premod": "¡traduceme!"
|
||||
"comment-post-notif": "Tu comentario ha sido publicado.",
|
||||
"comment-post-notif-premod": "Gracias por comentar. Nuestro equipo de moderación va a revisarlo muy pronto.",
|
||||
"comment-post-banned-word": "Tu comentario contiene una o más palabras que no estan permitidasen nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import FlagButton from './FlagButton';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const FlagBio = (props) => <FlagButton {...props} getPopupMenu={getPopupMenu} />;
|
||||
|
||||
const getPopupMenu = [
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-2-header'),
|
||||
itemType: 'user',
|
||||
field: 'bio',
|
||||
options: [
|
||||
{val: 'This bio is offensive', text: lang.t('bio-offensive')},
|
||||
{val: 'I don\'t like this bio', text: lang.t('no-like-bio')},
|
||||
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
|
||||
{val: 'other', text: lang.t('other')}
|
||||
],
|
||||
button: lang.t('continue'),
|
||||
sets: 'detail'
|
||||
};
|
||||
},
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-3-header'),
|
||||
text: lang.t('thank-you'),
|
||||
button: lang.t('done'),
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
export default FlagBio;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,47 +1,178 @@
|
||||
import React from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import {PopupMenu, Button} from 'coral-ui';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
|
||||
const flagged = flag && flag.current_user;
|
||||
const onFlagClick = () => {
|
||||
if (!currentUser) {
|
||||
class FlagButton extends Component {
|
||||
|
||||
state = {
|
||||
showMenu: false,
|
||||
showOther: false,
|
||||
itemType: '',
|
||||
detail: '',
|
||||
otherText: '',
|
||||
step: 0,
|
||||
posted: false
|
||||
}
|
||||
|
||||
// When the "report" button is clicked expand the menu
|
||||
onReportClick = () => {
|
||||
if (!this.props.currentUser) {
|
||||
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
return;
|
||||
}
|
||||
if (!flagged) {
|
||||
postAction(id, 'flag', currentUser.id, 'comments')
|
||||
this.setState({showMenu: !this.state.showMenu});
|
||||
}
|
||||
|
||||
onPopupContinue = () => {
|
||||
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
|
||||
const {itemType, field, detail, step, otherText, posted} = this.state;
|
||||
|
||||
//Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
this.setState({showMenu: false});
|
||||
} else {
|
||||
this.setState({step: step + 1});
|
||||
}
|
||||
|
||||
// If itemType and detail are both set, post the action
|
||||
if (itemType && detail && !posted) {
|
||||
// Set the text from the "other" field if it exists.
|
||||
const updatedDetail = otherText || detail;
|
||||
let item_id;
|
||||
switch(itemType) {
|
||||
case 'comments':
|
||||
item_id = id;
|
||||
break;
|
||||
case 'user':
|
||||
item_id = author_id;
|
||||
break;
|
||||
}
|
||||
const action = {
|
||||
action_type: 'flag',
|
||||
field,
|
||||
detail: updatedDetail
|
||||
};
|
||||
postAction(item_id, itemType, action)
|
||||
.then((action) => {
|
||||
let id = `${action.action_type}_${action.item_id}`;
|
||||
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, id, 'comments');
|
||||
updateItem(action.item_id, action.action_type, id, action.item_type);
|
||||
this.setState({posted: true});
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif'));
|
||||
} else {
|
||||
deleteAction(flagged.id)
|
||||
.then(() => {
|
||||
updateItem(id, 'flag', '', 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif-remove'));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onFlagClick} className={`${name}-button`}>
|
||||
onPopupOptionClick = (sets) => (e) => {
|
||||
|
||||
// If the "other" option is clicked, show the other textbox
|
||||
if(sets === 'detail' && e.target.value === 'other') {
|
||||
this.setState({showOther: true});
|
||||
}
|
||||
|
||||
// If flagging a user, indicate that this is referencing the username rather than the bio
|
||||
if(sets === 'itemType' && e.target.value === 'user') {
|
||||
this.setState({field: 'username'});
|
||||
}
|
||||
|
||||
// Set itemType and field if they are defined in the popupMenu
|
||||
const currentMenu = this.props.getPopupMenu[this.state.step]();
|
||||
if (currentMenu.itemType) {
|
||||
this.setState({itemType: currentMenu.itemType});
|
||||
}
|
||||
if (currentMenu.field) {
|
||||
this.setState({field: currentMenu.field});
|
||||
}
|
||||
|
||||
this.setState({[sets]: e.target.value});
|
||||
}
|
||||
|
||||
onOtherTextChange = (e) => {
|
||||
this.setState({otherText: e.target.value});
|
||||
}
|
||||
|
||||
handleClickOutside () {
|
||||
this.setState({showMenu: false});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {flag, getPopupMenu} = this.props;
|
||||
const flagged = flag && flag.current_user;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={this.onReportClick} className={`${name}-button`}>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
|
||||
this.state.showMenu &&
|
||||
<div className={`${name}-popup`}>
|
||||
<PopupMenu>
|
||||
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
|
||||
{
|
||||
popupMenu.text &&
|
||||
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
|
||||
}
|
||||
{
|
||||
popupMenu.options && <form className={`${name}-popup-form`}>
|
||||
{
|
||||
popupMenu.options.map((option) =>
|
||||
<div key={option.val}>
|
||||
<input
|
||||
className={`${name}-popup-radio`}
|
||||
type="radio"
|
||||
id={option.val}
|
||||
checked={this.state[popupMenu.sets] === option.val}
|
||||
onClick={this.onPopupOptionClick(popupMenu.sets)}
|
||||
value={option.val}/>
|
||||
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.showOther && <div>
|
||||
<input
|
||||
className={`${name}-other-text`}
|
||||
type="text"
|
||||
id="otherText"
|
||||
onChange={this.onOtherTextChange}
|
||||
value={this.state.otherText}/>
|
||||
<label htmlFor={'otherText'} className={`${name}-popup-radio-label screen-reader-text`}>
|
||||
lang.t('flag-reason')
|
||||
</label><br/>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
<div className={`${name}-popup-counter`}>
|
||||
{this.state.step + 1} of {getPopupMenu.length}
|
||||
</div>
|
||||
{
|
||||
popupMenu.button && <Button
|
||||
className={`${name}-popup-button`}
|
||||
onClick={this.onPopupContinue}>
|
||||
{popupMenu.button}
|
||||
</Button>
|
||||
}
|
||||
</PopupMenu>
|
||||
</div>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default FlagButton;
|
||||
export default onClickOutside(FlagButton);
|
||||
|
||||
const styles = {
|
||||
flaggedIcon: {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import FlagButton from './FlagButton';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const FlagComment = (props) => <FlagButton {...props} getPopupMenu={getPopupMenu} />;
|
||||
|
||||
const getPopupMenu = [
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-1-header'),
|
||||
options: [
|
||||
{val: 'user', text: lang.t('flag-username')},
|
||||
{val: 'comments', text: lang.t('flag-comment')}
|
||||
],
|
||||
button: lang.t('continue'),
|
||||
sets: 'itemType'
|
||||
};
|
||||
},
|
||||
(itemType) => {
|
||||
const options = itemType === 'comments' ?
|
||||
[
|
||||
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
|
||||
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
|
||||
{val: 'This comment reveals personally identifiable infomration', text: lang.t('personal-info')},
|
||||
{val: 'other', text: lang.t('other')}
|
||||
]
|
||||
: [
|
||||
{val: 'This username is offensive', text: lang.t('username-offensive')},
|
||||
{val: 'I don\'t like this username', text: lang.t('no-like-username')},
|
||||
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
|
||||
{val: 'other', text: lang.t('other')}
|
||||
];
|
||||
return {
|
||||
header: lang.t('step-2-header'),
|
||||
options,
|
||||
button: lang.t('continue'),
|
||||
sets: 'detail'
|
||||
};
|
||||
},
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-3-header'),
|
||||
text: lang.t('thank-you'),
|
||||
button: lang.t('done'),
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
export default FlagComment;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,14 +1,50 @@
|
||||
{
|
||||
"en": {
|
||||
"flag": "Flag",
|
||||
"flagged": "Flagged",
|
||||
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
|
||||
"flag-notif-remove": "Your flag has been removed."
|
||||
"report": "Report",
|
||||
"reported": "Reported",
|
||||
"report-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
|
||||
"report-notif-remove": "Your report has been removed.",
|
||||
"step-1-header": "Report an issue",
|
||||
"step-2-header": "Help us understand",
|
||||
"step-3-header": "Thank you for your input",
|
||||
"flag-username": "Flag username",
|
||||
"flag-comment": "Flag comment",
|
||||
"continue": "Continue",
|
||||
"done": "Done",
|
||||
"no-agree-comment": "I don't agree with this comment",
|
||||
"comment-offensive": "This comment is offensive",
|
||||
"personal-info": "This comment reveals personally identifiable information",
|
||||
"username-offensive": "This username is offensive",
|
||||
"no-like-username": "I don't like this username",
|
||||
"bio-offensive": "This bio is offensive",
|
||||
"no-like-bio": "I don't like this bio",
|
||||
"marketing": "This looks like an ad/marketing",
|
||||
"thank-you": "We value your safety and feedback. A moderator will review your flag.",
|
||||
"flag-reason": "Reason for flag",
|
||||
"other": "Other"
|
||||
},
|
||||
"es": {
|
||||
"flag": "Marcar",
|
||||
"flagged": "Marcado",
|
||||
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
|
||||
"flag-notif-remove": "¡traduceme!"
|
||||
"report": "Informe",
|
||||
"reported": "Informado",
|
||||
"report-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
|
||||
"report-notif-remove": "Tu marca ha sido eliminada.",
|
||||
"step-1-header": "Reportar un problema",
|
||||
"step-2-header": "Ayudanos a entender",
|
||||
"step-3-header": "Gracias por tu participación",
|
||||
"flag-username": "Marcar el nombre de usuario",
|
||||
"flag-comment": "Marcar el comentario",
|
||||
"continue": "Continuar",
|
||||
"done": "hecho",
|
||||
"no-agree-comment": "No estoy de acuerdo con este comentario",
|
||||
"comment-offensive": "Este comentario es ofensivo",
|
||||
"personal-info": "Este comentario muestra información personal",
|
||||
"username-offensive": "Este nombre de usuario es ofensivo",
|
||||
"no-like-username": "No me gusta ese nombre de usuario",
|
||||
"bio-offensive": "Esta bio es ofensiva",
|
||||
"no-like-bio": "No me gusta esta bio",
|
||||
"marketing": "Esto parece una publicidad/marketing",
|
||||
"thank-you": "Nos interesa tu protección y comentarios. Un moderador va a mirar tu marca.",
|
||||
"flag-reason": "Razón por la que marcar",
|
||||
"other": "Otro"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,22 @@ import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
|
||||
const liked = like && like.current_user;
|
||||
const onLikeClick = () => {
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
|
||||
showSignInDialog(offset);
|
||||
return;
|
||||
}
|
||||
if (banned) {
|
||||
return;
|
||||
}
|
||||
if (!liked) {
|
||||
postAction(id, 'like', currentUser.id, 'comments')
|
||||
const action = {
|
||||
action_type: 'like'
|
||||
};
|
||||
postAction(id, 'comments', action)
|
||||
.then((action) => {
|
||||
let id = `${action.action_type}_${action.item_id}`;
|
||||
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
|
||||
|
||||
@@ -9,8 +9,8 @@ const lang = new I18n(translations);
|
||||
class PermalinkButton extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
asset_id: PropTypes.string.isRequired,
|
||||
comment_id: PropTypes.string.isRequired
|
||||
articleURL: PropTypes.string.isRequired,
|
||||
commentId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
@@ -43,29 +43,27 @@ class PermalinkButton extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const publisherUrl = `${location.protocol}//${location.host}/`;
|
||||
|
||||
return (
|
||||
<div className={`${name}-container`} style={styles}>
|
||||
<div className={`${name}-container`}>
|
||||
<button onClick={this.toggle} className={`${name}-button`}>
|
||||
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
|
||||
{lang.t('permalink.permalink')}
|
||||
</button>
|
||||
<div
|
||||
style={styles.popover(this.state.popoverOpen)}
|
||||
className={`${name}-popover`}>
|
||||
className={`${name}-popover ${this.state.popoverOpen ? 'active' : ''}`}>
|
||||
<input
|
||||
className={`${name}-copy-field`}
|
||||
type='text'
|
||||
ref={input => this.permalinkInput = input}
|
||||
value={`${publisherUrl}${this.props.asset_id}#${this.props.comment_id}`}
|
||||
value={`${this.props.articleURL}#${this.props.commentId}`}
|
||||
onChange={() => {}} />
|
||||
<button className={`${name}-copy-button`} onClick={this.copyPermalink}>Copy</button>
|
||||
{
|
||||
this.state.copySuccessful ? <p>copied to clipboard</p> : null
|
||||
this.state.copySuccessful ? <p className={`${name}-copied-text`}>copied to clipboard</p> : null
|
||||
}
|
||||
{
|
||||
this.state.copyFailure
|
||||
? <p>copying to clipboard not supported in this browser. Use Cmd + C.</p>
|
||||
? <p className={`${name}-copied-error`}>copying to clipboard not supported in this browser. Use Cmd + C.</p>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
@@ -75,20 +73,3 @@ class PermalinkButton extends React.Component {
|
||||
}
|
||||
|
||||
export default onClickOutside(PermalinkButton);
|
||||
|
||||
const styles = {
|
||||
position: 'relative',
|
||||
|
||||
popover: active => {
|
||||
return {
|
||||
display: active ? 'block' : 'none',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid black',
|
||||
minWidth: 400,
|
||||
position: 'absolute',
|
||||
top: 30,
|
||||
right: 0,
|
||||
padding: 5
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,12 @@ const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyButton = (props) => <button
|
||||
className={`${name}-reply-button`}
|
||||
onClick={() => props.updateItem(props.id, 'showReply', !props.showReply, 'comments')}>
|
||||
onClick={() => {
|
||||
if (props.banned) {
|
||||
return;
|
||||
}
|
||||
props.updateItem(props.id, 'showReply', !props.showReply, 'comments');
|
||||
}}>
|
||||
{lang.t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>reply</i>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.bio textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
min-height: 100px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #d8d8d8;
|
||||
}
|
||||
|
||||
.bio h1 {
|
||||
font-size: 16px;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.bio p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './Bio.css';
|
||||
import {Button} from '../../coral-ui';
|
||||
|
||||
export default ({bio, handleSave, handleInput, handleCancel}) => (
|
||||
<div className={styles.bio}>
|
||||
<h1>Bio</h1>
|
||||
<p>Tell the community about yourself</p>
|
||||
<form>
|
||||
<textarea value={bio} onChange={handleInput} />
|
||||
<div className={styles.actions}>
|
||||
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
|
||||
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentHistory.css';
|
||||
|
||||
export default ({comments = []}) => (
|
||||
<div className={styles.header}>
|
||||
<h1>Comments</h1>
|
||||
<ul>
|
||||
{comments.map(() => (
|
||||
<li>
|
||||
{/* Comment Data*/}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
.message {
|
||||
padding: 10px 0 20px;
|
||||
letter-spacing: 0.1px;
|
||||
font-size: 13px;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.message a {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import styles from './NotLoggedIn.css';
|
||||
|
||||
export default ({showSignInDialog}) => (
|
||||
<div className={styles.message}>
|
||||
<div>
|
||||
<a onClick={showSignInDialog}>Sign In</a> to access Settings
|
||||
</div>
|
||||
<div>
|
||||
From the Settings Page you can
|
||||
<ul>
|
||||
<li>See your comment history</li>
|
||||
<li>Write a bio about yourself to display to the community</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user