diff --git a/.circleci/config.yml b/.circleci/config.yml index 848d496e7..5d22aa7df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,9 +41,9 @@ integration_job: &integration_job - attach_workspace: at: ~/coralproject/talk - <<: *create_indexes - - run: - name: Setup the database with defaults - command: ./bin/cli setup --defaults + # - run: + # name: Setup the database with defaults + # command: ./bin/cli setup --defaults - run: name: Run the integration tests command: bash .circleci/e2e.sh diff --git a/.eslintrc.json b/.eslintrc.json index d00d21106..d2356ace2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,5 +2,10 @@ "env": { "jest": true }, + "settings": { + "react": { + "version": "15.0" + } + }, "extends": "@coralproject/eslint-config-talk" } diff --git a/.nsprc b/.nsprc index 530a17a9f..8962db719 100644 --- a/.nsprc +++ b/.nsprc @@ -7,6 +7,7 @@ "https://nodesecurity.io/advisories/594", "https://nodesecurity.io/advisories/603", "https://nodesecurity.io/advisories/611", - "https://nodesecurity.io/advisories/612" + "https://nodesecurity.io/advisories/612", + "https://nodesecurity.io/advisories/654" ] } diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..2f954597a --- /dev/null +++ b/.nvmrc @@ -0,0 +1,2 @@ +8 + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..10041faa8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +overrides: +- files: "bin/cli*" + options: + parser: babylon diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 449ef585b..d066d7657 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,78 +1,6 @@ -# Contributor Covenant Code of Conduct +# Mozilla Community Participation Guidelines -## Our Pledge +## Version 2.3 - Updated May 16, 2017 -We expect everyone contributing to The Coral Project to follow this code of conduct. That means the team, contractors we employ, contributors, as well as anyone posting to our public or internal-facing channels. -We created it not because we anticipate any unacceptable behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team, and because having a code provides us with clear avenues to correct our culture should it ever stray from that course. - -We commit to enforce and evolve this code over the duration of the project. - -## Expected behavior - -* Be supportive of each other. -* Be collaborative. Involve others in brainstorms, sketching sessions, code reviews, planning documents, and the like. It’s not only okay to ask for help or feedback often, it’s unacceptable not to do so. -* Be generous and kind in both giving and accepting critique. Critique is a natural and important part of our culture. Good critiques are kind, respectful, clear, and constructive, focused on goals and requirements rather than personal preferences. You are expected to give and receive criticism with grace. -* Be humane. Be polite and friendly in all forms of communication, especially remote communication, where opportunities for misunderstanding are greater. Use sarcasm carefully. Tone is hard to decipher online; make judicious use of emoji to aid in communication. -* Be considerate. -* Be tolerant. -* Respect people’s boundaries. -* Do not make it personal. -* Use welcoming and inclusive language. -* Offer to help if you see someone struggling or otherwise in need of assistance (taking care not to be patronizing or disrespectful). -* If someone approaches you looking for help, be generous with your time; if you’re under a deadline, direct them to someone else who may be of assistance. -* Go out of your way to include people in jokes or memes, recognizing that we want to build an environment free of cliques. -* Show empathy towards other community members - - -## Unacceptable behavior - -We are committed to providing a welcoming and safe environment for people of all races, gender identities, gender expressions, sexual orientations, physical abilities, physical appearances, socioeconomic backgrounds, nationalities, ages, religions, and beliefs. -We expect that you will refrain from demeaning, discriminatory, or harassing behavior and speech. - -Harassment includes, but is not limited to: deliberate intimidation; stalking; unwanted photography or recording; sustained or willful disruption of talks or other events; inappropriate physical contact; use of sexual or discriminatory imagery, comments, or jokes; and unwelcome sexual attention. -Furthermore, any behavior or language which is unwelcoming—whether or not it rises to the level of harassment—is also strongly discouraged. Much exclusionary behavior takes the form of microaggressions—subtle put-downs which may be unconsciously delivered. Regardless of intent, microaggressions can have a significant negative impact on victims and have no place on our team. - -Other inappropriate behavior: - -* Threats -* Slurs -* Pornography -* Spam -* Bullying -* Copyright infringement -* Impersonation of someone else -* Violating someone’s privacy - -If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org). - - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -These guidelines are ambitious, and we’re not always going to succeed in meeting them. When something goes wrong—whether it’s a microaggression or an instance of harassment — there are a number of things you can do to address the situation. Depending on your comfort level and the severity of the situation, here are some suggestions: - -* Address it directly. If you’re comfortable bringing up the incident with the person who instigated it, pull them aside to discuss how it affected you. Be sure to approach these conversations in a forgiving spirit: an angry or tense conversation will not do either of you any good. If you’re unsure how to go about that, try discussing with your manager or with the people and culture team first—they might have some advice about how to make this conversation happen. - -If you’re too frustrated to have a direct conversation, there are a number of alternate routes you can take. - -* Talk to a peer or mentor. Your colleagues are likely to have personal and professional experience on which to draw that could be of use to you. If you have someone you’re comfortable approaching, reach out and discuss the situation with them. They may be able to advise on how they would handle it, or direct you to someone who can. The flip side of this, of course, is that you should also be available when your colleagues reach out to you. - -* Contact the project lead, Andrew Losowsky, [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org), or the technical lead. We will work with you to help you figure out how to ensure that any conflict doesn’t interfere with your work, in confidence if you would prefer. - -* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at [clawrence@mozillafoundation.org](mailto:clawrence@mozillafoundation.org). - -If you feel you have been unfairly accused of violating this code of conduct, you should contact Chris with a concise description of your grievance. - -## Conclusion - -We welcome your feedback on this and every other aspect of what we do as The Coral Project, and we thank you for working with us to make it a safe, enjoyable, and friendly experience for everyone involved in the project and what we do. -Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSD’s code of conduct, Vox Media’s product team code of conduct, Medium’s code of conduct, as well as adapted from the Contributor Covenant. \ No newline at end of file +The heart of Mozilla is people. We put people first and do our best to recognize, appreciate and respect the diversity of our global contributors. The Mozilla Project welcomes contributions from everyone who shares our goals and wants to contribute in a healthy and constructive manner within our community. As such, we have adopted this code of conduct and require all those who participate to agree and adhere to these Community Participation Guidelines in order to help us create a safe and positive community experience for all. +https://www.mozilla.org/en-US/about/governance/policies/participation/ diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 3e837aad6..ac88bd509 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -6,9 +6,12 @@ ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10 ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3 ONBUILD ARG TALK_THREADING_LEVEL=3 ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all +ONBUILD ARG TALK_DISABLE_EMBED_POLYFILL=FALSE ONBUILD ARG TALK_DEFAULT_LANG=en +ONBUILD ARG TALK_WHITELISTED_LANGUAGES ONBUILD ARG TALK_PLUGINS_JSON ONBUILD ARG TALK_WEBPACK_SOURCE_MAP +ONBUILD ARG TALK_DEFAULT_LAZY_RENDER # Bundle app source ONBUILD COPY . /usr/src/app @@ -20,4 +23,4 @@ ONBUILD COPY . /usr/src/app ONBUILD RUN cli plugins reconcile && \ yarn && \ yarn build && \ - yarn cache clean \ No newline at end of file + yarn cache clean diff --git a/README.md b/README.md index b0bc6e497..0e39cc780 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,12 @@ -# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946/badge)](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests) +# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests) -Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html). +Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/talk). -Built with <3 by The Coral Project & Mozilla. +Built with <3 by The Coral Project. -## Try Talk! +## Getting Started -You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time. - -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) - -## Technical Documentation - -From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://docs.coralproject.net/talk/) have everything you need to know. +Check out our Quickstart and Install guides to get started with Talk in our [Technical Docs](https://docs.coralproject.net/talk/). ## Product Guide @@ -20,13 +14,20 @@ Learn more about Talk, including a deep dive into features for commenters and mo ## Pre-Launch Guide -You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/). +You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/). + +## Advanced Usage + +For advanced configuration and usage of Talk, check out our [Configuration](https://docs.coralproject.net/talk/advanced-configuration/) and [Integration](https://docs.coralproject.net/talk/integrating/authentication/) how-tos. This covers topics in which you will need dev support to fully customize and integrate Talk, such as SSO/authentication, creating and managing assets and articles, styling Talk with custom CSS, and setting up Notifications and SMTP support. + +## Versions & Upgrading + +The current recommended release version is v4.5.0. ^4.5 (and all future even-numbered versions) are considered stable LTS versions. We recommend ^4.5 for use in production environments. ## More Resources - [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625) -- [Our Blog](https://blog.coralproject.net/) -- [Community Forums](https://community.coralproject.net/) +- [Our Blog](https://coralproject.net/blog) - [Community Guides for Journalism](https://guides.coralproject.net/) - [More About Us](https://coralproject.net/) diff --git a/app.js b/app.js index c5507da2e..b3400b939 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,6 @@ const express = require('express'); +const nunjucks = require('nunjucks'); +const cons = require('consolidate'); const trace = require('./middleware/trace'); const logging = require('./middleware/logging'); const path = require('path'); @@ -72,7 +74,26 @@ app.use( // VIEW CONFIGURATION //============================================================================== -app.set('views', path.join(__dirname, 'views')); +// configure the default views directory. +const views = path.join(__dirname, 'views'); +app.set('views', views); + +// reconfigure nunjucks. +cons.requires.nunjucks = nunjucks.configure(views, { + autoescape: true, + trimBlocks: true, + lstripBlocks: true, + watch: process.env.NODE_ENV === 'development', +}); + +// assign the nunjucks engine to .njk files. +app.engine('njk', cons.nunjucks); + +// assign the ejs engine to .ejs and .html files. +app.engine('ejs', cons.ejs); +app.engine('html', cons.ejs); + +// set .ejs as the default extension. app.set('view engine', 'ejs'); //============================================================================== diff --git a/app.json b/app.json index 6c85e3cc2..9f245105f 100644 --- a/app.json +++ b/app.json @@ -18,6 +18,10 @@ "value": "", "required": true }, + "MAILGUN_SMTP_PASSWORD": { + "value": "", + "required": true + }, "NODE_ENV": "production", "REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" diff --git a/bin/cli-assets b/bin/cli-assets index 5c1afb741..d626ba094 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -7,7 +7,7 @@ const util = require('./util'); const program = require('commander'); const parseDuration = require('ms'); -const Table = require('cli-table'); +const Table = require('cli-table2'); const AssetModel = require('../models/asset'); const CommentModel = require('../models/comment'); const AssetsService = require('../services/assets'); @@ -219,6 +219,28 @@ async function rewrite(search, replace, options) { } } +/** + * debugAssetURL will scrape the given URL and print it's scraped values. + */ +async function debugAssetURL(url) { + try { + const meta = await scraper.scrape(url); + + const table = new Table({ head: ['Property', 'Value'] }); + + for (const [property, value] of Object.entries(meta)) { + table.push([property, value]); + } + + console.log(table.toString()); + + util.shutdown(0); + } catch (err) { + console.error(err); + util.shutdown(1); + } +} + //============================================================================== // Setting up the program command line arguments. //============================================================================== @@ -234,6 +256,13 @@ program .description('list all the assets in the database') .action(listAssets); +program + .command('debug [url]') + .description( + 'prints the scraped metadata that would be added to the given asset' + ) + .action(debugAssetURL); + program .command('refresh [age]') .description('queues the assets that exceed the age requested') diff --git a/bin/cli-plugins b/bin/cli-plugins index f2e76de21..f2ca03350 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -143,9 +143,9 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) { if (fetchable.length > 0) { console.log( - `$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable.map( - ({ name, version }) => `${name}@${version}`.cyan - )}` + `$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable + .map(({ name, version }) => `${name}@${version}`.cyan) + .join(' ')}` ); if (!dryRun) { @@ -162,7 +162,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) { if (output.status) { throw new Error( - 'Could not install external plugins, errors occured during install' + 'Could not install external plugins, errors occurred during install' ); } @@ -189,7 +189,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) { if (output.status) { throw new Error( - 'Could not install external plugins, errors occured during install' + 'Could not install external plugins, errors occurred during install' ); } @@ -329,28 +329,41 @@ async function createSeedPlugin() { if (answers.addPluginsJson) { const pluginsJson = path.resolve(__dirname, '..', 'plugins.json'); - fs - .readJson(pluginsJson) - .then(j => { - // This is a client-side plugin, let's push this. - if (answers.client) { - j.client.push(answers.pluginName); + let j; + try { + j = await fs.readJson(pluginsJson); + } catch (err) { + // Fallback to plugins.default.json if the plugins.json file does not + // exist. + const defaultPluginsJson = path.resolve( + __dirname, + '..', + 'plugins.default.json' + ); - const output = JSON.stringify(j, null, 2); - fs.writeFileSync(pluginsJson, output); - } + try { + j = await fs.readJson(defaultPluginsJson); + } catch (err) { + // Fallback to an empty one if that also, doesn't exist. + j = { client: [], server: [] }; + } + } - // This is a server-side plugin, let's push this. - if (answers.server) { - j.server.push(answers.pluginName); + // This is a client-side plugin, let's push this. + if (answers.client) { + j.client.push(answers.pluginName); - const output = JSON.stringify(j, null, 2); - fs.writeFileSync(pluginsJson, output); - } - }) - .catch(err => { - console.error(err); - }); + const output = JSON.stringify(j, null, 2); + fs.writeFileSync(pluginsJson, output); + } + + // This is a server-side plugin, let's push this. + if (answers.server) { + j.server.push(answers.pluginName); + + const output = JSON.stringify(j, null, 2); + fs.writeFileSync(pluginsJson, output); + } } console.log( diff --git a/bin/cli-serve b/bin/cli-serve index d40edc9e4..0a63aa6cf 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,8 +1,10 @@ #!/usr/bin/env node +const throng = require('throng'); +const { CONCURRENCY } = require('../config'); +const { logger } = require('../services/logging'); const util = require('./util'); const program = require('commander'); -const serve = require('../serve'); //============================================================================== // Setting up the program command line arguments. @@ -22,8 +24,18 @@ program ) .parse(process.argv); -// Start serving. -serve(program).catch(err => { - console.error(err); - util.shutdown(1); +throng({ + workers: CONCURRENCY, + start: i => { + logger.info({ workerID: i }, 'started worker'); + + // Load in the serve. + const serve = require('../serve'); + + // Start serving. + serve(program).catch(err => { + console.error(err); + util.shutdown(1); + }); + }, }); diff --git a/bin/cli-setup b/bin/cli-setup index 11da478c2..d2db3b30d 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -45,11 +45,10 @@ const performSetup = async () => { } catch (err) { // If the error is `not init`, then we're good, otherwise, it's something // else. - if (err instanceof ErrSettingsNotInit) { + if (!err instanceof ErrSettingsNotInit) { + throw err; return; } - - throw err; } if (program.defaults) { @@ -138,7 +137,7 @@ const performSetup = async () => { console.log("\nWe'll ask you some questions about your first admin user.\n"); - let user = await inquirer.prompt([ + let { username, email } = await inquirer.prompt([ { type: 'input', name: 'username', @@ -161,39 +160,46 @@ const performSetup = async () => { return 'Email is required'; }, }, - { - name: 'password', - message: 'Password', - type: 'password', - filter: password => { - return UsersService.isValidPassword(password).catch(err => { - throw err.message; - }); - }, - }, - { - name: 'confirmPassword', - message: 'Confirm Password', - type: 'password', - filter: (confirmPassword, { password }) => { - if (password !== confirmPassword) { - return Promise.reject(new Error('Passwords do not match')); - } - - return UsersService.isValidPassword(confirmPassword).catch(err => { - throw err.message; - }); - }, - }, ]); + let password = ''; + while (!password) { + answers = await inquirer.prompt([ + { + name: 'password', + message: 'Password', + type: 'password', + filter: password => { + try { + UsersService.isValidPassword(password); + } catch (err) { + throw err.message; + } + + return password; + }, + }, + { + name: 'confirmPassword', + message: 'Confirm Password', + type: 'password', + }, + ]); + + if (answers.password !== answers.confirmPassword) { + console.error('Passwords do not match'); + } else { + password = answers.password; + } + } + const ctx = Context.forSystem(); let { user: newUser } = await SetupService.setup(ctx, { settings: settings.toObject(), user: { - email: user.email, - username: user.username, - password: user.password, + email, + username, + password, }, }); diff --git a/bin/cli-token b/bin/cli-token index b131728e6..2590ec106 100755 --- a/bin/cli-token +++ b/bin/cli-token @@ -8,7 +8,7 @@ const util = require('./util'); const program = require('commander'); const mongoose = require('../services/mongoose'); const TokensService = require('../services/tokens'); -const Table = require('cli-table'); +const Table = require('cli-table2'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); @@ -49,7 +49,10 @@ async function revokeToken(tokenID) { async function createToken(userID, tokenName) { try { - let { pat: { id }, jwt } = await TokensService.create(userID, tokenName); + let { + pat: { id }, + jwt, + } = await TokensService.create(userID, tokenName); console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`); diff --git a/bin/cli-users b/bin/cli-users index bf34cce97..743119a0a 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -8,7 +8,7 @@ const util = require('./util'); const program = require('commander'); const inquirer = require('inquirer'); const { stripIndent } = require('common-tags'); -const Table = require('cli-table'); +const Table = require('cli-table2'); // Make things colorful! require('colors'); diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 9a0452ea1..c7c7cf2a3 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -1,4 +1,4 @@ -import queryString from 'query-string'; +import queryString from 'querystringify'; import { FETCH_USERS_REQUEST, diff --git a/client/coral-admin/src/actions/rejectUsernameDialog.js b/client/coral-admin/src/actions/rejectUsernameDialog.js new file mode 100644 index 000000000..946b36ef7 --- /dev/null +++ b/client/coral-admin/src/actions/rejectUsernameDialog.js @@ -0,0 +1,14 @@ +import { + SHOW_REJECT_USERNAME_DIALOG, + HIDE_REJECT_USERNAME_DIALOG, +} from '../constants/rejectUsernameDialog'; + +export const showRejectUsernameDialog = ({ userId, username }) => ({ + type: SHOW_REJECT_USERNAME_DIALOG, + userId, + username, +}); + +export const hideRejectUsernameDialog = () => ({ + type: HIDE_REJECT_USERNAME_DIALOG, +}); diff --git a/client/coral-admin/src/actions/stories.js b/client/coral-admin/src/actions/stories.js index ab610ae20..9927f3cf0 100644 --- a/client/coral-admin/src/actions/stories.js +++ b/client/coral-admin/src/actions/stories.js @@ -1,4 +1,4 @@ -import queryString from 'query-string'; +import queryString from 'querystringify'; import { FETCH_ASSETS_REQUEST, diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js index 81b857a13..52370f9a7 100644 --- a/client/coral-admin/src/components/ActionsMenu.js +++ b/client/coral-admin/src/components/ActionsMenu.js @@ -76,7 +76,7 @@ ActionsMenu.propTypes = { icon: PropTypes.string, children: PropTypes.node, className: PropTypes.string, - label: PropTypes.string, + label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), buttonClassNames: PropTypes.string, }; diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js index 7370ba466..54816147a 100644 --- a/client/coral-admin/src/components/ActionsMenuItem.js +++ b/client/coral-admin/src/components/ActionsMenuItem.js @@ -15,7 +15,7 @@ const ActionsMenuItem = props => ( ActionsMenuItem.propTypes = { className: PropTypes.string, - children: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), }; export default ActionsMenuItem; diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js index 99bd61696..e6bcb3b89 100644 --- a/client/coral-admin/src/components/AdminLogin.js +++ b/client/coral-admin/src/components/AdminLogin.js @@ -5,6 +5,7 @@ import styles from './NotFound.css'; import { Button, TextField, Alert, Success } from 'coral-ui'; import Recaptcha from 'react-recaptcha'; import cn from 'classnames'; +import t from 'coral-framework/services/i18n'; class AdminLogin extends React.Component { constructor(props) { @@ -41,13 +42,13 @@ class AdminLogin extends React.Component { {errorMessage && {errorMessage}} this.setState({ email: e.target.value })} /> this.setState({ password: e.target.value })} type="password" @@ -60,10 +61,10 @@ class AdminLogin extends React.Component { full onClick={this.handleSignIn} > - Sign In + {t('login.sign_in_button')}

- Forgot your password?{' '} + {t('login.forgot_password')}{' '} - Request a new one. + {t('login.request_passowrd')}

{loginMaxExceeded && ( @@ -95,14 +96,14 @@ class AdminLogin extends React.Component { > {this.props.passwordRequestSuccess}{' '} - Sign in + {t('login.sign_in')}

) : (
this.setState({ email: e.target.value })} /> @@ -112,17 +113,15 @@ class AdminLogin extends React.Component { full onClick={this.handleRequestPassword} > - Reset Password + {t('login.reset_password')} ); return (
-

Team sign in

-

- Sign in to interact with your community. -

+

{t('login.team_sign_in')}

+

{t('login.sign_in_message')}

{this.state.requestPassword ? requestPasswordForm : signInForm}
diff --git a/client/coral-admin/src/components/App.css b/client/coral-admin/src/components/App.css new file mode 100644 index 000000000..39c7cf942 --- /dev/null +++ b/client/coral-admin/src/components/App.css @@ -0,0 +1,11 @@ +:global { + html, body, #root, #root > div { + min-height: 100%; + } + + body { + margin: 0; + background-color: #FAFAFA; + font-family: 'Roboto', sans-serif; + } +} diff --git a/client/coral-admin/src/components/App.js b/client/coral-admin/src/components/App.js index 52535f91a..a7ab1e336 100644 --- a/client/coral-admin/src/components/App.js +++ b/client/coral-admin/src/components/App.js @@ -1,5 +1,6 @@ import React from 'react'; import ToastContainer from './ToastContainer'; +import './App.css'; import 'material-design-lite'; import AppRouter from '../AppRouter'; diff --git a/client/coral-admin/src/components/ApproveButton.js b/client/coral-admin/src/components/ApproveButton.js index 884ac3da5..96dbdd375 100644 --- a/client/coral-admin/src/components/ApproveButton.js +++ b/client/coral-admin/src/components/ApproveButton.js @@ -14,7 +14,8 @@ const ApproveButton = ({ active, minimal, onClick, className, disabled }) => { className={cn( styles.root, { [styles.minimal]: minimal, [styles.active]: active }, - className + className, + 'talk-admin-approve-button' )} onClick={onClick} disabled={disabled || active} diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js index 430ee2fe4..fd8636320 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -19,7 +19,9 @@ class BanUserDialog extends React.Component { } handleMessageChange = e => { - const { value: message } = e; + const { + target: { value: message }, + } = e; this.setState({ message }); }; @@ -30,6 +32,12 @@ class BanUserDialog extends React.Component { }); }; + handlePerform = () => { + this.props.onPerform({ + message: this.state.message, + }); + }; + renderStep0() { const { onCancel, username, info } = this.props; @@ -63,7 +71,7 @@ class BanUserDialog extends React.Component { } renderStep1() { - const { onCancel, onPerform } = this.props; + const { onCancel } = this.props; const { message } = this.state; return ( @@ -95,7 +103,7 @@ class BanUserDialog extends React.Component {

- Go back to{' '} + {t('login.go_back')}{' '} - Sign In + {t('login.sign_in')} .

diff --git a/client/coral-admin/src/components/Header.js b/client/coral-admin/src/components/Header.js index fc150b840..f11b5a430 100644 --- a/client/coral-admin/src/components/Header.js +++ b/client/coral-admin/src/components/Header.js @@ -7,7 +7,6 @@ import styles from './Header.css'; import t from 'coral-framework/services/i18n'; import { Logo } from './Logo'; import { can } from 'coral-framework/services/perms'; -import ModerationIndicator from '../routes/Moderation/containers/Indicator'; import CommunityIndicator from '../routes/Community/containers/Indicator'; const CoralHeader = ({ @@ -32,7 +31,6 @@ const CoralHeader = ({ activeClassName={styles.active} > {t('configure.moderate')} - )} showShortcuts(true)}> {t('configure.shortcuts')} + + + {t('configure.product_guide_link')} + + - View latest version + {t('configure.view_last_version')} @@ -94,7 +101,7 @@ const CoralHeader = ({ target="_blank" rel="noopener noreferrer" > - Report a bug or give feedback + {t('configure.report_bug_or_feedback')} {currentUser && ( diff --git a/client/coral-admin/src/components/Indicator.css b/client/coral-admin/src/components/Indicator.css index ec84c7027..361f9ca5e 100644 --- a/client/coral-admin/src/components/Indicator.css +++ b/client/coral-admin/src/components/Indicator.css @@ -1,10 +1,8 @@ .indicator { + display: inline-block; background-color: #E46D59; border-radius: 10px; - position: absolute; - top: 50%; width: 7px; height: 7px; - margin-top: -4px; margin-left: 7px; } diff --git a/client/coral-admin/src/components/KarmaTooltip.css b/client/coral-admin/src/components/KarmaTooltip.css new file mode 100644 index 000000000..27afe7b38 --- /dev/null +++ b/client/coral-admin/src/components/KarmaTooltip.css @@ -0,0 +1,103 @@ +.karmaTooltip { + position: relative; + display: inline-block; + margin: 2px 4px 0; +} + +.icon { + font-size: 16px; + color: #0D5B8F; + user-select: none; + -webkit-tap-highlight-color:rgba(0,0,0,0); + + > i { + vertical-align: baseline; + } +} + +.icon:hover { + cursor: pointer; +} + +.menu { + background-color: white; + border: solid 1px #999; + border-radius: 3px; + padding: 10px; + position: absolute; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); + z-index: 10; + top: 32px; + left: -100px; + width: 150px; + text-align: left; + color: #616161; +} + +.menu::before{ + content: ''; + border: 10px solid transparent; + border-top-color: #999; + position: absolute; + left: 96px; + top: -20px; + transform: rotate(180deg); +} + +.menu::after{ + content: ''; + border: 10px solid transparent; + border-top-color: white; + position: absolute; + left: 96px; + top: -19px; + transform: rotate(180deg); +} + +.menu ul { + list-style: none; + padding: 0; + + li { + display: flex; + justify-content: space-between; + margin: 5px 0; + } +} + +.label { + padding: 4px 5px; + border-radius: 3px; + color: #fff; + font-weight: 400; + text-align: center; + font-size: .9em; + line-height: normal; + letter-spacing: .4px; + min-width: 25px; + display: block; + + /* &.reliable { background-color: #03AB61; } */ + /* &.neutral { background-color: #616161; } */ + &.unreliable { background-color: #F44336; } +} + +.descriptionList { + padding: 0; + margin: 0; + list-style: none; +} + +.strongItem { + margin-right: 3px; +} + +.descriptionItem { + font-size: 0.9em; +} + +.link { + color: #2B7EB5; + text-decoration: underline; + display: block; +} diff --git a/client/coral-admin/src/components/KarmaTooltip.js b/client/coral-admin/src/components/KarmaTooltip.js new file mode 100644 index 000000000..d27597325 --- /dev/null +++ b/client/coral-admin/src/components/KarmaTooltip.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import { Icon } from 'coral-ui'; +import styles from './KarmaTooltip.css'; +import ClickOutside from 'coral-framework/components/ClickOutside'; +import t from 'coral-framework/services/i18n'; + +const initialState = { menuVisible: false }; + +class KarmaTooltip extends React.Component { + static propTypes = { + thresholds: PropTypes.shape({ + reliable: PropTypes.number.isRequired, + unreliable: PropTypes.number.isRequired, + }).isRequired, + }; + + state = initialState; + + toogleMenu = () => { + this.setState({ menuVisible: !this.state.menuVisible }); + }; + + hideMenu = () => { + this.setState({ menuVisible: false }); + }; + + render() { + const { + thresholds: { unreliable }, + } = this.props; + const { menuVisible } = this.state; + + return ( + +
+ + + + + {menuVisible && ( +
+ {t('user_detail.user_karma_score')} +
    + {/* NOTE: we may display this data in the future, keeping around for that eventuality */} + {/*
  • + Reliable{' '} + + ≥ {reliable} + +
  • +
  • + Neutral{' '} + + < {reliable}, > {unreliable} + +
  • */} +
  • + {t('user_detail.unreliable')}{' '} + + ≤ {unreliable} + +
  • +
+ + {t('user_detail.learn_more')} + +
+ )} +
+
+ ); + } +} + +export default KarmaTooltip; diff --git a/client/coral-admin/src/components/Login.js b/client/coral-admin/src/components/Login.js index cc5ec9436..2f3f9a975 100644 --- a/client/coral-admin/src/components/Login.js +++ b/client/coral-admin/src/components/Login.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import styles from './Login.css'; import Layout from 'coral-admin/src/components/Layout'; import cn from 'classnames'; +import t from 'coral-framework/services/i18n'; class LoginContainer extends Component { renderForm() { @@ -19,8 +20,8 @@ class LoginContainer extends Component { return (
-

Team sign in

-

Sign in to interact with your community.

+

{t('login.team_sign_in')}

+

{t('login.sign_in_message')}

{this.renderForm()}
diff --git a/client/coral-admin/src/components/RejectButton.js b/client/coral-admin/src/components/RejectButton.js index 69e1047e6..2aabff007 100644 --- a/client/coral-admin/src/components/RejectButton.js +++ b/client/coral-admin/src/components/RejectButton.js @@ -14,7 +14,8 @@ const RejectButton = ({ active, minimal, onClick, className, disabled }) => { className={cn( styles.root, { [styles.minimal]: minimal, [styles.active]: active }, - className + className, + 'talk-admin-reject-button' )} onClick={onClick} disabled={disabled || active} diff --git a/client/coral-admin/src/components/RejectUsernameDialog.css b/client/coral-admin/src/components/RejectUsernameDialog.css new file mode 100644 index 000000000..57c9ca24a --- /dev/null +++ b/client/coral-admin/src/components/RejectUsernameDialog.css @@ -0,0 +1,80 @@ +.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: 400px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; +} + +.header { + color: black; + font-size: 1.5em; + font-weight: 500; + margin: 0 0 8px 0; +} + +.close { + display: block; + position: absolute; + top: 24px; + right: 20px; +} + +.closeButton { + font-size: 24px; + line-height: 14px; + color: #363636; + &:hover { + color: #6b6b6b; + } +} + +.legend { + padding: 0; + font-weight: bold; +} + +div.radioGroup { + margin-top: 6px; +} + +label.radioGroup { + + &:global(.is-checked) > :global(.mdl-radio__outer-circle), + > :global(.mdl-radio__outer-circle) { + border-color: #212121; + } + + > :global(.mdl-radio__inner-circle) { + background: #212121; + } + + > :global(.mdl-radio__label) { + font-size: 14px; + line-height: 20px; + } +} + +.messageInput { + border-radius: 3px; + width: 100%; + padding: 10px; + font-size: 14px; + box-sizing: border-box; +} + +.cancel { + margin-right: 5px; +} + +.perform { + min-width: 90px; +} + +.buttons { + margin-top: 8px; + margin-bottom: 6px; + text-align: right; +} diff --git a/client/coral-admin/src/components/RejectUsernameDialog.js b/client/coral-admin/src/components/RejectUsernameDialog.js new file mode 100644 index 000000000..b14384eb8 --- /dev/null +++ b/client/coral-admin/src/components/RejectUsernameDialog.js @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dialog, BareButton } from 'coral-ui'; +import styles from './RejectUsernameDialog.css'; +import cn from 'classnames'; +import { RadioGroup, Radio } from 'react-mdl'; +import Button from 'coral-ui/components/Button'; +import { username as flagReason } from 'coral-framework/graphql/flagReasons'; +import t from 'coral-framework/services/i18n'; + +const initialState = { reason: flagReason.offensive, message: '' }; + +class RejectUsernameDialog extends React.Component { + state = initialState; + + componentWillReceiveProps(next) { + if (this.props.open && !next.open) { + this.setState(initialState); + } + } + + handleReasonChange = event => { + this.setState({ reason: event.target.value }); + }; + + handleMessageChange = event => { + this.setState({ message: event.target.value }); + }; + + handlePerform = () => { + this.props.onPerform({ + reason: this.state.reason, + message: this.state.message, + }); + }; + + render() { + const { open, onCancel } = this.props; + const { reason, message } = this.state; + return ( + +
+ + × + +
+
+

+ {t('reject_username_dialog.title')}: {this.props.username} +

+

+ {t('reject_username_dialog.description')} +

+
+ + {t('reject_username_dialog.reason')} + + + + {t('flag_reasons.username.offensive')} + + + {t('flag_reasons.username.nolike')} + + + {t('flag_reasons.username.impersonating')} + + + {t('flag_reasons.username.spam')} + + + {t('flag_reasons.username.other')} + + + {reason === flagReason.other && ( +
+ + {t('reject_username_dialog.message')} + +