diff --git a/.circleci/config.yml b/.circleci/config.yml index d280fda2c..9e1ee91e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,7 @@ job_defaults: &job_defaults integration_environment: &integration_environment NODE_ENV: test CIRCLE_TEST_REPORTS: /tmp/circleci-test-results + E2E_MAX_RETRIES: 3 # integration_job runs the integration tests and saves the test results. integration_job: &integration_job @@ -29,6 +30,7 @@ integration_job: &integration_job name: Run the integration tests command: bash .circleci/e2e.sh - store_test_results: + when: always path: /tmp/circleci-test-results version: 2 @@ -104,13 +106,19 @@ jobs: name: Setup the test results directory command: mkdir -p /tmp/circleci-test-results - run: - name: Run the unit tests - command: yarn test + name: Run the client unit tests + command: yarn test:client --ci environment: - MOCHA_FILE: /tmp/circleci-test-results/junit/test-results.xml + JEST_JUNIT_OUTPUT: /tmp/circleci-test-results/jest/test-results.xml + JEST_REPORTER: jest-junit + - run: + name: Run the server unit tests + command: yarn test:server + environment: + MOCHA_FILE: /tmp/circleci-test-results/mocha/test-results.xml MOCHA_REPORTER: mocha-junit-reporter - NODE_ENV: test - store_test_results: + when: always path: /tmp/circleci-test-results # test_integration_chrome_local will run the integration tests locally with @@ -164,6 +172,8 @@ jobs: <<: *integration_environment BROWSERSTACK: true E2E_BROWSERS: ie + # TODO: remove when more reliable + E2E_MAX_RETRIES: 1 # test_integration_safari will run the integration tests with safari in # browserstack. @@ -173,6 +183,8 @@ jobs: <<: *integration_environment BROWSERSTACK: true E2E_BROWSERS: safari + # TODO: remove when more reliable + E2E_MAX_RETRIES: 1 # deploy will deploy the application as a docker image. deploy: @@ -182,7 +194,7 @@ jobs: - setup_remote_docker - run: name: Deploy the code - command: bash ./scripts/docker.sh + command: bash ./scripts/docker.sh deploy # filter_deploy will add the filters for a deploy job in a workflow to make it # only execute on a deploy related job. @@ -274,4 +286,7 @@ workflows: - test_unit - test_integration_chrome - test_integration_firefox - - test_integration_edge \ No newline at end of file + - test_integration_edge + # TODO: uncomment when more reliable + # - test_integration_ie + # - test_integration_safari \ No newline at end of file diff --git a/.gitignore b/.gitignore index 84d6850ad..4004a5ede 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ plugins/* !plugins/talk-plugin-mod !plugins/talk-plugin-moderation-actions !plugins/talk-plugin-notifications +!plugins/talk-plugin-notifications-category-featured !plugins/talk-plugin-notifications-category-reply !plugins/talk-plugin-notifications-category-staff !plugins/talk-plugin-offtopic diff --git a/bin/cli-plugins b/bin/cli-plugins index ee331bff2..f2e76de21 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -143,7 +143,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) { if (fetchable.length > 0) { console.log( - `$ yarn add --ignore-scripts ${fetchable.map( + `$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable.map( ({ name, version }) => `${name}@${version}`.cyan )}` ); @@ -152,6 +152,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) { let args = [ 'add', '--ignore-scripts', + '--ignore-workspace-root-check', ...fetchable.map(({ name, version }) => `${name}@${version}`), ]; diff --git a/docs/_docs/04-03-additional-plugins.md b/docs/_docs/04-03-additional-plugins.md index 4f1654a4f..b88d8437c 100644 --- a/docs/_docs/04-03-additional-plugins.md +++ b/docs/_docs/04-03-additional-plugins.md @@ -149,3 +149,11 @@ Source: [plugins/talk-plugin-notifications-category-reply](https://github.com/co Replies made to each user will trigger an email to be sent with the notification details if enabled. + +### talk-plugin-notifications-category-featured +{:.param} + +Source: [plugins/talk-plugin-notifications-category-featured](https://github.com/coralproject/talk/tree/master/plugins/talk-plugin-notifications-category-featured){:target="_blank"} + +When a comment is featured (via the `talk-plugin-featured-comments` plugin), the +user will receive a notification email. diff --git a/jest.config.js b/jest.config.js index 654b72cd0..7150d1066 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,6 +27,8 @@ module.exports = { '\\.ya?ml$': '/test/client/yamlTransformer.js', }, + testResultsProcessor: process.env.JEST_REPORTER, + moduleNameMapper: { '^plugin-api\\/(.*)$': '/plugin-api/$1', '^plugins\\/(.*)$': '/plugins/$1', diff --git a/package.json b/package.json index 9c5f58ff5..e54ad11d4 100644 --- a/package.json +++ b/package.json @@ -216,6 +216,7 @@ "identity-obj-proxy": "^3.0.0", "ip": "^1.1.5", "jest": "^21.2.1", + "jest-junit": "^3.6.0", "lint-staged": "^7.0.0", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", diff --git a/plugins/talk-plugin-notifications-category-featured/client/.eslintrc.json b/plugins/talk-plugin-notifications-category-featured/client/.eslintrc.json new file mode 100644 index 000000000..c8a6db18a --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk/client" +} diff --git a/plugins/talk-plugin-notifications-category-featured/client/containers/Toggle.js b/plugins/talk-plugin-notifications-category-featured/client/containers/Toggle.js new file mode 100644 index 000000000..ec7586b16 --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/client/containers/Toggle.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { compose, gql } from 'react-apollo'; +import Toggle from 'talk-plugin-notifications/client/components/Toggle'; +import { t } from 'plugin-api/beta/client/services'; +import { withFragments } from 'plugin-api/beta/client/hocs'; + +class ToggleContainer extends React.Component { + constructor(props) { + super(props); + props.setTurnOffInputFragment({ onFeatured: false }); + + if (this.getOnFeaturedSetting()) { + props.indicateOn(); + } + } + + componentWillReceiveProps(nextProps) { + const prevSetting = this.getOnFeaturedSetting(this.props); + const nextSetting = this.getOnFeaturedSetting(nextProps); + if (prevSetting && !nextSetting) { + nextProps.indicateOff(); + } else if (!prevSetting && nextSetting) { + nextProps.indicateOn(); + } + } + + getOnFeaturedSetting = (props = this.props) => + props.root.me.notificationSettings.onFeatured; + + toggle = () => { + this.props.updateNotificationSettings({ + onFeatured: !this.getOnFeaturedSetting(), + }); + }; + + render() { + return ( + + {t('talk-plugin-notifications-category-featured.toggle_description')} + + ); + } +} + +ToggleContainer.propTypes = { + data: PropTypes.object, + root: PropTypes.object, + indicateOn: PropTypes.func.isRequired, + indicateOff: PropTypes.func.isRequired, + setTurnOffInputFragment: PropTypes.func.isRequired, + updateNotificationSettings: PropTypes.func.isRequired, +}; + +const enhance = compose( + withFragments({ + root: gql` + fragment TalkNotificationsCategoryFeatured_Toggle_root on RootQuery { + me { + notificationSettings { + onFeatured + } + } + } + `, + }) +); + +export default enhance(ToggleContainer); diff --git a/plugins/talk-plugin-notifications-category-featured/client/graphql.js b/plugins/talk-plugin-notifications-category-featured/client/graphql.js new file mode 100644 index 000000000..440a6f7fa --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/client/graphql.js @@ -0,0 +1,33 @@ +import { gql } from 'react-apollo'; + +export default { + mutations: { + UpdateNotificationSettings: ({ + variables: { input }, + state: { auth: { user: { id } } }, + }) => ({ + update: proxy => { + if (input.onFeatured === undefined) { + return; + } + + const fragment = gql` + fragment TalkNotificationsCategoryFeatured_User_Fragment on User { + notificationSettings { + onFeatured + } + } + `; + const fragmentId = `User_${id}`; + const data = { + __typename: 'User', + notificationSettings: { + __typename: 'NotificationSettings', + onFeatured: input.onFeatured, + }, + }; + proxy.writeFragment({ fragment, id: fragmentId, data }); + }, + }), + }, +}; diff --git a/plugins/talk-plugin-notifications-category-featured/client/index.js b/plugins/talk-plugin-notifications-category-featured/client/index.js new file mode 100644 index 000000000..1e22c3a93 --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/client/index.js @@ -0,0 +1,11 @@ +import Toggle from './containers/Toggle'; +import translations from './translations.yml'; +import graphql from './graphql'; + +export default { + slots: { + notificationSettings: [Toggle], + }, + translations, + ...graphql, +}; diff --git a/plugins/talk-plugin-notifications-category-featured/client/translations.yml b/plugins/talk-plugin-notifications-category-featured/client/translations.yml new file mode 100644 index 000000000..7077943ef --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/client/translations.yml @@ -0,0 +1,3 @@ +en: + talk-plugin-notifications-category-featured: + toggle_description: My comment is featured diff --git a/plugins/talk-plugin-notifications-category-featured/index.js b/plugins/talk-plugin-notifications-category-featured/index.js new file mode 100644 index 000000000..802c37a9a --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/index.js @@ -0,0 +1,118 @@ +const { graphql } = require('graphql'); +const { get } = require('lodash'); +const path = require('path'); + +const handle = async (ctx, { comment }) => { + const { connectors: { graph: { schema } } } = ctx; + + // Check to see if this is a reply to an existing comment. + const commentID = get(comment, 'id', null); + if (commentID === null) { + ctx.log.debug('could not get comment id'); + return; + } + + // Execute the graph request. + const reply = await graphql( + schema, + ` + query GetAuthorUserMetadata($comment_id: ID!) { + comment(id: $comment_id) { + id + user { + id + notificationSettings { + onFeatured + } + } + } + } + `, + {}, + ctx, + { comment_id: commentID } + ); + if (reply.errors) { + ctx.log.error({ err: reply.errors }, 'could not query for author metadata'); + return; + } + + // Check if the user has notifications enabled. + const enabled = get( + reply, + 'data.comment.user.notificationSettings.onFeatured', + false + ); + if (!enabled) { + return; + } + + const userID = get(reply, 'data.comment.user.id', null); + if (!userID) { + ctx.log.debug('could not get comment user id'); + return; + } + + // The user does have notifications for featured comments enabled, queue the + // notification to be sent. + return { userID, date: comment.created_at, context: comment.id }; +}; + +const hydrate = async (ctx, category, context) => { + const { connectors: { graph: { schema } } } = ctx; + + const reply = await graphql( + schema, + ` + query GetNotificationData($context: ID!) { + comment(id: $context) { + id + asset { + title + url + } + } + } + `, + {}, + ctx, + { context } + ); + if (reply.errors) { + throw reply.errors; + } + + const comment = get(reply, 'data.comment'); + const headline = get(comment, 'asset.title', null); + const assetURL = get(comment, 'asset.url', null); + const permalink = `${assetURL}?commentId=${comment.id}`; + + return [headline, permalink]; +}; + +const handler = { + handle, + category: 'featured', + event: 'commentFeatured', + hydrate, +}; + +module.exports = { + typeDefs: ` + type NotificationSettings { + onFeatured: Boolean! + } + + input NotificationSettingsInput { + onFeatured: Boolean + } + `, + resolvers: { + NotificationSettings: { + // onFeatured returns false by default if not specified. + onFeatured: settings => get(settings, 'onFeatured', false), + }, + }, + translations: path.join(__dirname, 'translations.yml'), + notifications: [handler], +}; diff --git a/plugins/talk-plugin-notifications-category-featured/translations.yml b/plugins/talk-plugin-notifications-category-featured/translations.yml new file mode 100644 index 000000000..bd0d7412f --- /dev/null +++ b/plugins/talk-plugin-notifications-category-featured/translations.yml @@ -0,0 +1,6 @@ +en: + talk-plugin-notifications: + categories: + featured: + subject: "One of your comments was featured on [{0}]" + body: "{0}\nA member of our team has selected this comment to be featured for other readers: {1}" \ No newline at end of file diff --git a/plugins/talk-plugin-notifications-category-reply/.eslintrc.json b/plugins/talk-plugin-notifications-category-reply/.eslintrc.json deleted file mode 100644 index 78f7c2397..000000000 --- a/plugins/talk-plugin-notifications-category-reply/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@coralproject/eslint-config-talk" -} diff --git a/plugins/talk-plugin-notifications/.eslintrc.json b/plugins/talk-plugin-notifications/.eslintrc.json deleted file mode 100644 index 78f7c2397..000000000 --- a/plugins/talk-plugin-notifications/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@coralproject/eslint-config-talk" -} diff --git a/plugins/talk-plugin-notifications/client/containers/Settings.js b/plugins/talk-plugin-notifications/client/containers/Settings.js index ff07c6433..e987bafa2 100644 --- a/plugins/talk-plugin-notifications/client/containers/Settings.js +++ b/plugins/talk-plugin-notifications/client/containers/Settings.js @@ -23,9 +23,9 @@ class SettingsContainer extends React.Component { hasNotifications: this.state.hasNotifications.filter(i => i !== plugin), }); setTurnOffInputFragment = fragment => - this.setState({ - turnOffInput: { ...this.state.turnOffInput, ...fragment }, - }); + this.setState(state => ({ + turnOffInput: { ...state.turnOffInput, ...fragment }, + })); turnOffAll = () => { this.props.updateNotificationSettings(this.state.turnOffInput); diff --git a/yarn.lock b/yarn.lock index 62eb054fb..5153185a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5399,6 +5399,14 @@ jest-jasmine2@^22.4.2: jest-util "^22.4.1" source-map-support "^0.5.0" +jest-junit@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-3.6.0.tgz#f4c4358e5286364a4324dc14abddd526aadfbd38" + dependencies: + mkdirp "^0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.1" + jest-matcher-utils@^21.2.1: version "21.2.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64" @@ -10987,7 +10995,7 @@ xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" -xml@^1.0.0: +xml@^1.0.0, xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"