From 3c3a0523de8816a77da51dbcdd52cf6dd9ffb92d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 10 Jul 2017 16:27:15 -0300 Subject: [PATCH 01/47] Avatar support --- .../client/components/MyPluginComponent.ejs | 25 ++ .../plugin/client/components/style.ejs | 27 +++ .../src/components/Comment.css | 11 + .../src/components/Comment.js | 217 ++++++++++-------- package.json | 2 + webpack.config.js | 4 - yarn.lock | 33 ++- 7 files changed, 212 insertions(+), 107 deletions(-) create mode 100644 bin/templates/plugin/client/components/MyPluginComponent.ejs create mode 100644 bin/templates/plugin/client/components/style.ejs diff --git a/bin/templates/plugin/client/components/MyPluginComponent.ejs b/bin/templates/plugin/client/components/MyPluginComponent.ejs new file mode 100644 index 000000000..5732cd146 --- /dev/null +++ b/bin/templates/plugin/client/components/MyPluginComponent.ejs @@ -0,0 +1,25 @@ +import React from 'react'; +import {CoralLogo} from 'plugin-api/beta/client/components/ui'; +import styles from './style.css'; + +class MyPluginComponent extends React.Component { + render() { + return ( +
+ +
+

<%= pluginName %> Plugin created by Talk CLI

+ + + To read more about plugins check{' '} + + our docs and guides! + + +
+
+ ); + } +} + +export default MyPluginComponent; diff --git a/bin/templates/plugin/client/components/style.ejs b/bin/templates/plugin/client/components/style.ejs new file mode 100644 index 000000000..187e68750 --- /dev/null +++ b/bin/templates/plugin/client/components/style.ejs @@ -0,0 +1,27 @@ +.myPluginContainer { + padding: 10px; + background: #f0f0f0; + border: 1px solid #d6d6d6; + margin: 10px 0; + text-align: center; + border-radius: 3px; +} + +.logo { + position: block; + animation: spin 2s infinite ease; + animation-delay: 1s; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.description { + color: #444444; +} diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 4c8a9fc86..9b96f74b5 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -11,6 +11,7 @@ .comment { padding-left: 20px; + flex: auto; } .commentLevel0 { @@ -141,3 +142,13 @@ .enter { animation: enter 1000ms; } + +.commentRow { + display: flex; + flex-flow: row; +} + +.commentAvatar { + max-width: 50px; + margin-right: 10px; +} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 7cc8b1480..460834169 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -436,127 +436,140 @@ export default class Comment extends React.Component { id={`c_${comment.id}`} > {!isReply &&
} -
- - {isStaff(comment.tags) ? Staff : null} - - {commentIsBest(comment) - ? - : null } - - - - { - (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null - } - - + +
+
+ + {isStaff(comment.tags) ? Staff : null} - { (currentUser && (comment.user.id === currentUser.id)) && + {commentIsBest(comment) + ? + : null } - /* User can edit/delete their own comment for a short window after posting */ - + + { - commentIsStillEditable(comment) && - Edit + (comment.editing && comment.editing.edited) + ?  ({t('comment.edited')}) + : null } - } - { (currentUser && (comment.user.id !== currentUser.id)) && - /* TopRightMenu allows currentUser to ignore other users' comments */ - - + + + { (currentUser && (comment.user.id === currentUser.id)) && + + /* User can edit/delete their own comment for a short window after posting */ + + { + commentIsStillEditable(comment) && + Edit + } - } - { - this.state.isEditing - ? + + + } + { + this.state.isEditing + ? + :
+ +
+ } + +
+ + + + + + + {!disableReply && + + + } +
+
+ - :
- -
- } - -
- - - - - - - {!disableReply && - - - } -
-
- - - - + + + +
+ {activeReplyBox === comment.id ? { diff --git a/package.json b/package.json index 218381896..6d792c70c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "env-rewrite": "^1.0.2", "express": "^4.15.2", "express-session": "^1.15.1", + "file-loader": "^0.11.2", "form-data": "^2.1.2", "fs-extra": "^3.0.1", "gql-merge": "^0.0.4", @@ -209,6 +210,7 @@ "style-loader": "^0.16.0", "supertest": "^2.0.1", "timeago.js": "^2.0.3", + "url-loader": "^0.5.9", "webpack": "^2.3.1" }, "engines": { diff --git a/webpack.config.js b/webpack.config.js index c39726882..5c6655d17 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -70,10 +70,6 @@ const config = { ], test: /.css$/, }, - { - loader: 'url-loader?limit=100000', - test: /\.png$/ - }, { loader: 'file-loader', test: /\.(jpg|png|gif|svg)$/ diff --git a/yarn.lock b/yarn.lock index 9de2c35fa..ee4d6c9eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3119,6 +3119,12 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" +file-loader@^0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34" + dependencies: + loader-utils "^1.0.2" + file-uri-to-path@0: version "0.0.2" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-0.0.2.tgz#37cdd1b5b905404b3f05e1b23645be694ff70f82" @@ -3290,6 +3296,14 @@ fs-extra@^0.26.4: path-is-absolute "^1.0.0" rimraf "^2.2.8" +fs-extra@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs-promise@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-0.3.1.tgz#bf34050368f24d6dc9dfc6688ab5cead8f86842a" @@ -4632,6 +4646,12 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -5227,7 +5247,7 @@ mime-types@~2.0.3: dependencies: mime-db "~1.12.0" -mime@1.3.4, mime@^1.3.4: +mime@1.3.4, mime@1.3.x, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -8263,6 +8283,10 @@ uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" +universalify@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.0.tgz#9eb1c4651debcc670cc94f1a75762332bb967778" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -8283,6 +8307,13 @@ urijs@1.16.1: version "1.16.1" resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.16.1.tgz#859ad31890f5f9528727be89f1932c94fb4731e2" +url-loader@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295" + dependencies: + loader-utils "^1.0.2" + mime "1.3.x" + url-search-params@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/url-search-params/-/url-search-params-0.9.0.tgz#e71d7764a6503533cbfe9771b2963cb61ea1c225" From f09550a4acd075411392dd0cdc321f23862aee06 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 10 Jul 2017 16:56:46 -0300 Subject: [PATCH 02/47] Avatar plugin support extending fragments --- .eslintignore | 1 + .gitignore | 1 + .../src/containers/Comment.js | 3 +- plugin-api/beta/client/hocs/index.js | 1 + plugins/talk-plugin-avatar/client/.babelrc | 14 ++++ .../talk-plugin-avatar/client/.eslintrc.json | 23 ++++++ .../client/assets/avatar-placeholder.png | Bin 0 -> 8811 bytes .../client/components/UserAvatar.js | 8 ++ .../client/components/styles.css | 4 + .../client/containers/UserAvatar.js | 12 +++ plugins/talk-plugin-avatar/client/index.js | 7 ++ plugins/talk-plugin-avatar/index.js | 75 ++++++++++++++++++ plugins/talk-plugin-avatar/package.json | 11 +++ 13 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 plugins/talk-plugin-avatar/client/.babelrc create mode 100644 plugins/talk-plugin-avatar/client/.eslintrc.json create mode 100644 plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png create mode 100644 plugins/talk-plugin-avatar/client/components/UserAvatar.js create mode 100644 plugins/talk-plugin-avatar/client/components/styles.css create mode 100644 plugins/talk-plugin-avatar/client/containers/UserAvatar.js create mode 100644 plugins/talk-plugin-avatar/client/index.js create mode 100644 plugins/talk-plugin-avatar/index.js create mode 100644 plugins/talk-plugin-avatar/package.json diff --git a/.eslintignore b/.eslintignore index 3a259c106..9ed3e5361 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,4 +13,5 @@ plugins/* !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content !plugins/talk-plugin-permalink +!plugins/talk-plugin-avatar node_modules diff --git a/.gitignore b/.gitignore index 0be1266c6..e94a01a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,6 @@ plugins/* !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content !plugins/talk-plugin-permalink +!plugins/talk-plugin-avatar **/node_modules/* diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 040d1a83e..1ffac82cf 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -10,7 +10,8 @@ const pluginFragments = getSlotsFragments([ 'commentInfoBar', 'commentActions', 'commentContent', - 'commentReactions' + 'commentReactions', + 'commentAvatar' ]); export default withFragments({ diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 7be682670..7f3476cfd 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -1 +1,2 @@ export {default as withReaction} from './withReaction'; +export {default as withFragments} from 'coral-framework/hocs/withFragments'; diff --git a/plugins/talk-plugin-avatar/client/.babelrc b/plugins/talk-plugin-avatar/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-avatar/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-avatar/client/.eslintrc.json b/plugins/talk-plugin-avatar/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-avatar/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png b/plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..2e828f0fb9c775fd824f571f78ee59e3f2f73ace GIT binary patch literal 8811 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 PKSg=4vUIhiaq#~E;=)iN literal 0 HcmV?d00001 diff --git a/plugins/talk-plugin-avatar/client/components/UserAvatar.js b/plugins/talk-plugin-avatar/client/components/UserAvatar.js new file mode 100644 index 000000000..ae1ab30f9 --- /dev/null +++ b/plugins/talk-plugin-avatar/client/components/UserAvatar.js @@ -0,0 +1,8 @@ +import React from 'react'; +import styles from './styles.css'; +import avatarPlaceholder from '../assets/avatar-placeholder.png'; + +const UserAvatar = ({comment: {user}}) => + + +export default UserAvatar; diff --git a/plugins/talk-plugin-avatar/client/components/styles.css b/plugins/talk-plugin-avatar/client/components/styles.css new file mode 100644 index 000000000..fc6111845 --- /dev/null +++ b/plugins/talk-plugin-avatar/client/components/styles.css @@ -0,0 +1,4 @@ +.avatarPlaceholder { + height: 50px; + width: 50px; +} \ No newline at end of file diff --git a/plugins/talk-plugin-avatar/client/containers/UserAvatar.js b/plugins/talk-plugin-avatar/client/containers/UserAvatar.js new file mode 100644 index 000000000..76b558e05 --- /dev/null +++ b/plugins/talk-plugin-avatar/client/containers/UserAvatar.js @@ -0,0 +1,12 @@ +import {gql} from 'react-apollo'; +import {withFragments} from 'plugin-api/beta/client/hocs'; +import UserAvatar from '../components/UserAvatar'; + +export default withFragments({ + comment: gql` + fragment UserAvatar_comment on Comment { + user { + avatar + } + }` +})(UserAvatar); diff --git a/plugins/talk-plugin-avatar/client/index.js b/plugins/talk-plugin-avatar/client/index.js new file mode 100644 index 000000000..922e464b8 --- /dev/null +++ b/plugins/talk-plugin-avatar/client/index.js @@ -0,0 +1,7 @@ +import UserAvatar from './components/UserAvatar'; + +export default { + slots: { + commentAvatar: [UserAvatar] + } +} \ No newline at end of file diff --git a/plugins/talk-plugin-avatar/index.js b/plugins/talk-plugin-avatar/index.js new file mode 100644 index 000000000..b91a58835 --- /dev/null +++ b/plugins/talk-plugin-avatar/index.js @@ -0,0 +1,75 @@ +// We need the UserModel because we need to update the user. +const UserModel = require('models/user'); + +// Get some middleware to use with the webhook. +const auth = require('middleware/authentication'); +const authz = require('middleware/authorization'); + +// Load some config from the environment. This could be changed to a settings +// option later if you want to go that route. +const DEFAULT_AVATAR = process.env.DEFAULT_AVATAR; + +module.exports = { + + // The new type definitions provides the new "avatar" field needed to inject + // into the User type. + typeDefs: ` + type User { + avatar: String + } + `, + + // The User resolver will return the avatar from the embedded user metadata. + resolvers: { + User: { + avatar(user) { + if (user && user.metadata && user.metadata.avatar) { + return user.metadata.avatar; + } + + return DEFAULT_AVATAR; + } + } + }, + + // The custom router routes that we add here will allow an external system to + // update the avatar when it changes on the remote system. Note that we do + // use the auth/authz middleware, checking for the ADMIN role. This can be + // used in conjunction with a personal access token generated from an ADMIN. + router(router) { + router.post('/webhooks/user_update', auth, authz.needed('ADMIN'), async (req, res, next) => { + + // We expect that the payload for the new avatar is in the following form: + // + // { + // "id": "123123-123123-12312313", + // "avatar": "https://great-cdn.cloudfront.net/best-photo.jpg" + // ... + // } + + + // Extract the data from the payload. + let { + id, + avatar + } = req.body; + + try { + + // Update the user model. + await UserModel.update({id}, { + $set: { + 'metadata.avatar': avatar + } + }); + + } catch (e) { + return next(e); + } + + // Respond with a `202 Accepted` to indicate that we were able to process + // the update. + res.status(202).end() + }); + } +}; \ No newline at end of file diff --git a/plugins/talk-plugin-avatar/package.json b/plugins/talk-plugin-avatar/package.json new file mode 100644 index 000000000..c652a86d0 --- /dev/null +++ b/plugins/talk-plugin-avatar/package.json @@ -0,0 +1,11 @@ +{ + "name": "talk-plugin-avatar", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Belen Curcio ", + "license": "ISC" +} From 2c7f5ca089873de0a9f1b7ab1178443698fe58bd Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 10 Jul 2017 17:00:01 -0300 Subject: [PATCH 03/47] linting --- plugins/talk-plugin-avatar/client/components/UserAvatar.js | 2 +- plugins/talk-plugin-avatar/client/index.js | 2 +- plugins/talk-plugin-avatar/index.js | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/talk-plugin-avatar/client/components/UserAvatar.js b/plugins/talk-plugin-avatar/client/components/UserAvatar.js index ae1ab30f9..a87669540 100644 --- a/plugins/talk-plugin-avatar/client/components/UserAvatar.js +++ b/plugins/talk-plugin-avatar/client/components/UserAvatar.js @@ -3,6 +3,6 @@ import styles from './styles.css'; import avatarPlaceholder from '../assets/avatar-placeholder.png'; const UserAvatar = ({comment: {user}}) => - + ; export default UserAvatar; diff --git a/plugins/talk-plugin-avatar/client/index.js b/plugins/talk-plugin-avatar/client/index.js index 922e464b8..a706a41d0 100644 --- a/plugins/talk-plugin-avatar/client/index.js +++ b/plugins/talk-plugin-avatar/client/index.js @@ -4,4 +4,4 @@ export default { slots: { commentAvatar: [UserAvatar] } -} \ No newline at end of file +}; diff --git a/plugins/talk-plugin-avatar/index.js b/plugins/talk-plugin-avatar/index.js index b91a58835..fca0aafee 100644 --- a/plugins/talk-plugin-avatar/index.js +++ b/plugins/talk-plugin-avatar/index.js @@ -47,7 +47,6 @@ module.exports = { // ... // } - // Extract the data from the payload. let { id, @@ -69,7 +68,7 @@ module.exports = { // Respond with a `202 Accepted` to indicate that we were able to process // the update. - res.status(202).end() + res.status(202).end(); }); } -}; \ No newline at end of file +}; From 636f35718632b4e2d006595d110e9e0302239e86 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 10 Jul 2017 17:06:40 -0300 Subject: [PATCH 04/47] Reverting changes from old branch --- bin/cli-plugins | 51 +------------------ .../client/components/MyPluginComponent.ejs | 25 --------- .../plugin/client/components/style.ejs | 27 ---------- client/coral-framework/helpers/plugins.js | 5 +- package.json | 1 - scripts/templates/plugin/client/.babelrc | 14 ----- .../templates/plugin/client/.eslintrc.json | 23 --------- scripts/templates/plugin/client/index.js | 20 -------- .../templates/plugin/client/translations.yml | 15 ------ scripts/templates/plugin/index.js | 1 - 10 files changed, 3 insertions(+), 179 deletions(-) delete mode 100644 bin/templates/plugin/client/components/MyPluginComponent.ejs delete mode 100644 bin/templates/plugin/client/components/style.ejs delete mode 100644 scripts/templates/plugin/client/.babelrc delete mode 100644 scripts/templates/plugin/client/.eslintrc.json delete mode 100644 scripts/templates/plugin/client/index.js delete mode 100644 scripts/templates/plugin/client/translations.yml delete mode 100644 scripts/templates/plugin/index.js diff --git a/bin/cli-plugins b/bin/cli-plugins index 6dc127697..b1dee381b 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -8,14 +8,13 @@ // https://yarnpkg.com/ const program = require('./commander'); -const inquirer = require('inquirer'); // Make things colorful! require('colors'); const emoji = require('node-emoji'); const dir = process.cwd(); -const fs = require('fs-extra'); +const fs = require('fs'); const path = require('path'); const spawn = require('cross-spawn'); const semver = require('semver'); @@ -273,58 +272,10 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote console.log(`✨ Done in ${totalTime}s.`); } -async function createSeedPlugin() { - const pluginsDir = path.join(dir, 'plugins'); - - function pluginNameExists(pluginName) { - const pluginNames = fs.readdirSync(pluginsDir); - return !!pluginNames - .filter((pn) => pn === pluginName).length; - } - - let answers = await inquirer.prompt([ - { - type: 'input', - name: 'pluginName', - message: 'Plugin Name: i.e talk-plugin-permalink', - validate: (input) => { - - if (pluginNameExists(input)) { - return 'Please, choose another name. That name already exists'; - } - - if (input && input.length > 0) { - return true; - } - - return 'Plugin Name is required.'; - } - } - ]); - - // Creating plugin seed - - const seedTemplatePlugin = path.join(dir, 'scripts/templates/plugin'); - const newPluginPath = path.join(pluginsDir, answers.pluginName); - - try { - fs.copySync(seedTemplatePlugin, newPluginPath); - - console.log('Success!'); - } catch (err) { - console.error(err); - } -} - //============================================================================== // Setting up the program command line arguments. //============================================================================== -program - .command('create') - .description('creates a seed plugin') - .action(createSeedPlugin); - program .command('list') .description('') diff --git a/bin/templates/plugin/client/components/MyPluginComponent.ejs b/bin/templates/plugin/client/components/MyPluginComponent.ejs deleted file mode 100644 index 5732cd146..000000000 --- a/bin/templates/plugin/client/components/MyPluginComponent.ejs +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import {CoralLogo} from 'plugin-api/beta/client/components/ui'; -import styles from './style.css'; - -class MyPluginComponent extends React.Component { - render() { - return ( -

- -
-

<%= pluginName %> Plugin created by Talk CLI

- - - To read more about plugins check{' '} - - our docs and guides! - - -
-
- ); - } -} - -export default MyPluginComponent; diff --git a/bin/templates/plugin/client/components/style.ejs b/bin/templates/plugin/client/components/style.ejs deleted file mode 100644 index 187e68750..000000000 --- a/bin/templates/plugin/client/components/style.ejs +++ /dev/null @@ -1,27 +0,0 @@ -.myPluginContainer { - padding: 10px; - background: #f0f0f0; - border: 1px solid #d6d6d6; - margin: 10px 0; - text-align: center; - border-radius: 3px; -} - -.logo { - position: block; - animation: spin 2s infinite ease; - animation-delay: 1s; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.description { - color: #444444; -} diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index fcd0e6db6..ab5e3080e 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -15,9 +15,9 @@ function getSlotComponents(slot) { // Filter out components that have been disabled in `plugin_config` return flatten(plugins - + // Filter out components that have been disabled in `plugin_config` - .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.plugin] || !pluginConfig[o.plugin].disable_components)) + .filter((o) => !pluginConfig || !pluginConfig[o.plugin] || !pluginConfig[o.plugin].disable_components) .filter((o) => o.module.slots[slot]) .map((o) => o.module.slots[slot])); @@ -78,7 +78,6 @@ export function getSlotsFragments(slots) { } const components = uniq(flattenDeep(slots.map((slot) => { return plugins - .filter((o) => o.module.slots) .filter((o) => o.module.slots[slot]) .map((o) => o.module.slots[slot]); }))); diff --git a/package.json b/package.json index 6d792c70c..e9a3a8430 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "express-session": "^1.15.1", "file-loader": "^0.11.2", "form-data": "^2.1.2", - "fs-extra": "^3.0.1", "gql-merge": "^0.0.4", "graphql": "^0.9.1", "graphql-errors": "^2.1.0", diff --git a/scripts/templates/plugin/client/.babelrc b/scripts/templates/plugin/client/.babelrc deleted file mode 100644 index 60be246eb..000000000 --- a/scripts/templates/plugin/client/.babelrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "presets": [ - "es2015" - ], - "plugins": [ - "add-module-exports", - "transform-class-properties", - "transform-decorators-legacy", - "transform-object-assign", - "transform-object-rest-spread", - "transform-async-to-generator", - "transform-react-jsx" - ] -} \ No newline at end of file diff --git a/scripts/templates/plugin/client/.eslintrc.json b/scripts/templates/plugin/client/.eslintrc.json deleted file mode 100644 index 9fe56bd14..000000000 --- a/scripts/templates/plugin/client/.eslintrc.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "mocha": true - }, - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - } - }, - "parser": "babel-eslint", - "plugins": [ - "react" - ], - "rules": { - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "no-console": ["warn", { "allow": ["warn", "error"] }] - } -} diff --git a/scripts/templates/plugin/client/index.js b/scripts/templates/plugin/client/index.js deleted file mode 100644 index 931cd722c..000000000 --- a/scripts/templates/plugin/client/index.js +++ /dev/null @@ -1,20 +0,0 @@ - -/** - This is a client index example file and it should look like this: - - ``` - import LoveButton from './components/LoveButton'; - - export default { - slots: { - commentReactions: [LoveButton] - }, - reducer, - translations - }; - ``` - - To read more info on how to build client plugins. Please, go to: https://coralproject.github.io/talk/plugins-client.html - */ - -export default {}; diff --git a/scripts/templates/plugin/client/translations.yml b/scripts/templates/plugin/client/translations.yml deleted file mode 100644 index d8407b801..000000000 --- a/scripts/templates/plugin/client/translations.yml +++ /dev/null @@ -1,15 +0,0 @@ -# -# This file is for translations and they should look like this: -# -# -# ``` -# en: -# coral-plugin-respect: -# respect: Respect -# respected: Respected -# es: -# coral-plugin-respect: -# respect: Respetar -# respected: Respetado -# ``` -# diff --git a/scripts/templates/plugin/index.js b/scripts/templates/plugin/index.js deleted file mode 100644 index f053ebf79..000000000 --- a/scripts/templates/plugin/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; From 932103c204c4be9217f1eb815f6f0458bff400a6 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 10 Jul 2017 17:08:49 -0300 Subject: [PATCH 05/47] updated yarn lock --- plugins/talk-plugin-avatar/yarn.lock | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/talk-plugin-avatar/yarn.lock diff --git a/plugins/talk-plugin-avatar/yarn.lock b/plugins/talk-plugin-avatar/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/plugins/talk-plugin-avatar/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + From 0c167fdfc477858a3108be8a87e4021b28a3a75b Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 10 Jul 2017 17:43:51 -0300 Subject: [PATCH 06/47] Removing talk-plugin-avatar --- .eslintignore | 2 +- .gitignore | 1 - plugins/talk-plugin-avatar/client/.babelrc | 14 ---- .../talk-plugin-avatar/client/.eslintrc.json | 23 ------ .../client/assets/avatar-placeholder.png | Bin 8811 -> 0 bytes .../client/components/UserAvatar.js | 8 -- .../client/components/styles.css | 4 - .../client/containers/UserAvatar.js | 12 --- plugins/talk-plugin-avatar/client/index.js | 7 -- plugins/talk-plugin-avatar/index.js | 74 ------------------ plugins/talk-plugin-avatar/package.json | 11 --- plugins/talk-plugin-avatar/yarn.lock | 4 - 12 files changed, 1 insertion(+), 159 deletions(-) delete mode 100644 plugins/talk-plugin-avatar/client/.babelrc delete mode 100644 plugins/talk-plugin-avatar/client/.eslintrc.json delete mode 100644 plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png delete mode 100644 plugins/talk-plugin-avatar/client/components/UserAvatar.js delete mode 100644 plugins/talk-plugin-avatar/client/components/styles.css delete mode 100644 plugins/talk-plugin-avatar/client/containers/UserAvatar.js delete mode 100644 plugins/talk-plugin-avatar/client/index.js delete mode 100644 plugins/talk-plugin-avatar/index.js delete mode 100644 plugins/talk-plugin-avatar/package.json delete mode 100644 plugins/talk-plugin-avatar/yarn.lock diff --git a/.eslintignore b/.eslintignore index 9ed3e5361..069144d94 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,5 +13,5 @@ plugins/* !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content !plugins/talk-plugin-permalink -!plugins/talk-plugin-avatar + node_modules diff --git a/.gitignore b/.gitignore index e94a01a6c..0be1266c6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,5 @@ plugins/* !plugins/coral-plugin-viewing-options !plugins/coral-plugin-comment-content !plugins/talk-plugin-permalink -!plugins/talk-plugin-avatar **/node_modules/* diff --git a/plugins/talk-plugin-avatar/client/.babelrc b/plugins/talk-plugin-avatar/client/.babelrc deleted file mode 100644 index 60be246eb..000000000 --- a/plugins/talk-plugin-avatar/client/.babelrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "presets": [ - "es2015" - ], - "plugins": [ - "add-module-exports", - "transform-class-properties", - "transform-decorators-legacy", - "transform-object-assign", - "transform-object-rest-spread", - "transform-async-to-generator", - "transform-react-jsx" - ] -} \ No newline at end of file diff --git a/plugins/talk-plugin-avatar/client/.eslintrc.json b/plugins/talk-plugin-avatar/client/.eslintrc.json deleted file mode 100644 index 9fe56bd14..000000000 --- a/plugins/talk-plugin-avatar/client/.eslintrc.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "mocha": true - }, - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - } - }, - "parser": "babel-eslint", - "plugins": [ - "react" - ], - "rules": { - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "no-console": ["warn", { "allow": ["warn", "error"] }] - } -} diff --git a/plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png b/plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png deleted file mode 100644 index 2e828f0fb9c775fd824f571f78ee59e3f2f73ace..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8811 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 PKSg=4vUIhiaq#~E;=)iN diff --git a/plugins/talk-plugin-avatar/client/components/UserAvatar.js b/plugins/talk-plugin-avatar/client/components/UserAvatar.js deleted file mode 100644 index a87669540..000000000 --- a/plugins/talk-plugin-avatar/client/components/UserAvatar.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import styles from './styles.css'; -import avatarPlaceholder from '../assets/avatar-placeholder.png'; - -const UserAvatar = ({comment: {user}}) => - ; - -export default UserAvatar; diff --git a/plugins/talk-plugin-avatar/client/components/styles.css b/plugins/talk-plugin-avatar/client/components/styles.css deleted file mode 100644 index fc6111845..000000000 --- a/plugins/talk-plugin-avatar/client/components/styles.css +++ /dev/null @@ -1,4 +0,0 @@ -.avatarPlaceholder { - height: 50px; - width: 50px; -} \ No newline at end of file diff --git a/plugins/talk-plugin-avatar/client/containers/UserAvatar.js b/plugins/talk-plugin-avatar/client/containers/UserAvatar.js deleted file mode 100644 index 76b558e05..000000000 --- a/plugins/talk-plugin-avatar/client/containers/UserAvatar.js +++ /dev/null @@ -1,12 +0,0 @@ -import {gql} from 'react-apollo'; -import {withFragments} from 'plugin-api/beta/client/hocs'; -import UserAvatar from '../components/UserAvatar'; - -export default withFragments({ - comment: gql` - fragment UserAvatar_comment on Comment { - user { - avatar - } - }` -})(UserAvatar); diff --git a/plugins/talk-plugin-avatar/client/index.js b/plugins/talk-plugin-avatar/client/index.js deleted file mode 100644 index a706a41d0..000000000 --- a/plugins/talk-plugin-avatar/client/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import UserAvatar from './components/UserAvatar'; - -export default { - slots: { - commentAvatar: [UserAvatar] - } -}; diff --git a/plugins/talk-plugin-avatar/index.js b/plugins/talk-plugin-avatar/index.js deleted file mode 100644 index fca0aafee..000000000 --- a/plugins/talk-plugin-avatar/index.js +++ /dev/null @@ -1,74 +0,0 @@ -// We need the UserModel because we need to update the user. -const UserModel = require('models/user'); - -// Get some middleware to use with the webhook. -const auth = require('middleware/authentication'); -const authz = require('middleware/authorization'); - -// Load some config from the environment. This could be changed to a settings -// option later if you want to go that route. -const DEFAULT_AVATAR = process.env.DEFAULT_AVATAR; - -module.exports = { - - // The new type definitions provides the new "avatar" field needed to inject - // into the User type. - typeDefs: ` - type User { - avatar: String - } - `, - - // The User resolver will return the avatar from the embedded user metadata. - resolvers: { - User: { - avatar(user) { - if (user && user.metadata && user.metadata.avatar) { - return user.metadata.avatar; - } - - return DEFAULT_AVATAR; - } - } - }, - - // The custom router routes that we add here will allow an external system to - // update the avatar when it changes on the remote system. Note that we do - // use the auth/authz middleware, checking for the ADMIN role. This can be - // used in conjunction with a personal access token generated from an ADMIN. - router(router) { - router.post('/webhooks/user_update', auth, authz.needed('ADMIN'), async (req, res, next) => { - - // We expect that the payload for the new avatar is in the following form: - // - // { - // "id": "123123-123123-12312313", - // "avatar": "https://great-cdn.cloudfront.net/best-photo.jpg" - // ... - // } - - // Extract the data from the payload. - let { - id, - avatar - } = req.body; - - try { - - // Update the user model. - await UserModel.update({id}, { - $set: { - 'metadata.avatar': avatar - } - }); - - } catch (e) { - return next(e); - } - - // Respond with a `202 Accepted` to indicate that we were able to process - // the update. - res.status(202).end(); - }); - } -}; diff --git a/plugins/talk-plugin-avatar/package.json b/plugins/talk-plugin-avatar/package.json deleted file mode 100644 index c652a86d0..000000000 --- a/plugins/talk-plugin-avatar/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "talk-plugin-avatar", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Belen Curcio ", - "license": "ISC" -} diff --git a/plugins/talk-plugin-avatar/yarn.lock b/plugins/talk-plugin-avatar/yarn.lock deleted file mode 100644 index fb57ccd13..000000000 --- a/plugins/talk-plugin-avatar/yarn.lock +++ /dev/null @@ -1,4 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - From c3ad47f6f66d2c924e1cfbaeff5df53825792459 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 12 Jul 2017 14:49:46 -0300 Subject: [PATCH 07/47] Linting --- .../src/components/Comment.css | 1 + .../src/components/Comment.js | 330 +++++++++--------- 2 files changed, 173 insertions(+), 158 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 86b0014bf..c9dbeac69 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -167,4 +167,5 @@ .commentAvatar { max-width: 50px; margin-right: 10px; + margin-top: 10px; } diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 0cd37a92a..e612affb0 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -429,27 +429,10 @@ export default class Comment extends React.Component { className={rootClassName} id={`c_${comment.id}`} > -

- - {isStaff(comment.tags) ? Staff : null} - - {commentIsBest(comment) - ? - : null } - - - - { - (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null - } - - +
- { (currentUser && (comment.user.id === currentUser.id)) && +
+ + {isStaff(comment.tags) ? Staff : null} - /* User can edit/delete their own comment for a short window after posting */ - + {commentIsBest(comment) + ? + : null } + + + { - commentIsStillEditable(comment) && - Edit + (comment.editing && comment.editing.edited) + ?  ({t('comment.edited')}) + : null } - } - { (currentUser && (comment.user.id !== currentUser.id)) && - /* TopRightMenu allows currentUser to ignore other users' comments */ - - + + + { (currentUser && (comment.user.id === currentUser.id)) && + + /* User can edit/delete their own comment for a short window after posting */ + + { + commentIsStillEditable(comment) && + Edit + } - } - { - this.state.isEditing - ? - :
- -
- } + } + { (currentUser && (comment.user.id !== currentUser.id)) && -
- - - - - - - {!disableReply && - - - } -
-
- - - - -
-
- {activeReplyBox === comment.id - ? { - setActiveReplyBox(''); - }} - charCountEnable={charCountEnable} - maxCharCount={maxCharCount} - setActiveReplyBox={setActiveReplyBox} - parentId={(depth < THREADING_LEVEL) ? comment.id : parentId} - addNotification={addNotification} - postComment={postComment} - currentUser={currentUser} - assetId={asset.id} - /> - : null} + /* TopRightMenu allows currentUser to ignore other users' comments */ + + + + } + { + this.state.isEditing + ? + :
+ +
+ } - - {view.map((reply) => { - return commentIsIgnored(reply) - ? - : + + + + + + + {!disableReply && + + + } +
+
+ + + + +
+
+
+ + {activeReplyBox === comment.id + ? { + setActiveReplyBox(''); + }} charCountEnable={charCountEnable} maxCharCount={maxCharCount} - showSignInDialog={showSignInDialog} - commentIsIgnored={commentIsIgnored} - liveUpdates={liveUpdates} - key={reply.id} - comment={reply} - />; - })} - -
- + setActiveReplyBox={setActiveReplyBox} + parentId={(depth < THREADING_LEVEL) ? comment.id : parentId} + addNotification={addNotification} + postComment={postComment} + currentUser={currentUser} + assetId={asset.id} + /> + : null} + + + {view.map((reply) => { + return commentIsIgnored(reply) + ? + : ; + })} + +
+
); From 518b7b53587f2c5770e6711741007b149b210837 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 12 Jul 2017 15:04:24 -0300 Subject: [PATCH 08/47] merge --- client/coral-framework/helpers/plugins.js | 35 ++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 2e5934bc4..625a0ee3f 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -10,17 +10,17 @@ import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers, getStore} from 'coral-framework/services/store'; import camelize from './camelize'; -function getSlotComponents(slot) { +export function getSlotComponents(slot) { const pluginConfig = getStore().getState().config.plugin_config; - // Filter out components that have been disabled in `plugin_config` return flatten(plugins - - // Filter out components that have slots and have been disabled in `plugin_config` - .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.plugin] || !pluginConfig[o.plugin].disable_components)) - .filter((o) => o.module.slots[slot]) - .map((o) => o.module.slots[slot])); + // Filter out components that have slots and have been disabled in `plugin_config` + .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) + + .filter((o) => o.module.slots[slot]) + .map((o) => o.module.slots[slot]) + ); } export function isSlotEmpty(slot) { @@ -78,8 +78,8 @@ export function getSlotsFragments(slots) { } const components = uniq(flattenDeep(slots.map((slot) => { return plugins - .filter((o) => o.module.slots ? o.module.slots[slot] : false) - .map((o) => o.module.slots[slot]); + .filter((o) => o.module.slots ? o.module.slots[slot] : false) + .map((o) => o.module.slots[slot]); }))); const fragments = getComponentFragments(components); @@ -113,7 +113,22 @@ export function injectPluginsReducers() { const reducers = merge( ...plugins .filter((o) => o.module.reducer) - .map((o) => ({[camelize(o.plugin)] : o.module.reducer})) + .map((o) => ({[camelize(o.name)] : o.module.reducer})) ); injectReducers(reducers); } + +function addMetaDataToSlotComponents() { + + // Add talkPluginName to Slot Components. + plugins.forEach((plugin) => { + const slots = plugin.module.slots; + slots && Object.keys(slots).forEach((slot) => { + slots[slot].forEach((component) => { + component.talkPluginName = plugin.name; + }); + }); + }); +} + +addMetaDataToSlotComponents(); From 919bdff40adbeba10a62de9beafbdd3565e49043 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Thu, 13 Jul 2017 09:26:09 -0400 Subject: [PATCH 09/47] Use less words, say same thing --- docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 180e172ac..d914bcf03 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,7 +17,7 @@ Talk consists of four distinct layers of code: ### Plugins -Talk plugins deliver the features and functionality that can be changed or removed. Much of the default functionality is delivered by plugins allowing developers to change behavior along product lines that we've found to be important. +Talk plugins deliver the features and functionality that can be changed or removed. Much of the default functionality is delivered by core plugins allowing developers to have control over any non-essential functionality. ### Plugin API From 3d666111b0a35745159dcf11d8bed016308c1b49 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Thu, 13 Jul 2017 09:26:37 -0400 Subject: [PATCH 10/47] Add metadata docs --- docs/_data/sidebars/talk_sidebar.yml | 30 +++++++++++++++------------- docs/architecture-metadata.md | 0 2 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 docs/architecture-metadata.md diff --git a/docs/_data/sidebars/talk_sidebar.yml b/docs/_data/sidebars/talk_sidebar.yml index 62660783c..ebbc6e006 100644 --- a/docs/_data/sidebars/talk_sidebar.yml +++ b/docs/_data/sidebars/talk_sidebar.yml @@ -37,20 +37,6 @@ entries: url: /install-setup.html output: web - - title: Architecture - output: web - folderitems: - - title: Overview - url: /architecture.html - output: web - - title: Tags - url: /architecture-tags.html - output: web - - title: cli - url: /architecture-cli.html - output: web - - - title: Plugins output: web folderitems: @@ -70,6 +56,22 @@ entries: url: /plugins-experimental.html output: web + - title: Architecture + output: web + folderitems: + - title: Overview + url: /architecture.html + output: web + - title: Tags + url: /architecture-tags.html + output: web + - title: Metadata API + url: /architecture-metadata.html + output: web + - title: cli + url: /architecture-cli.html + output: web + - title: Development output: web folderitems: diff --git a/docs/architecture-metadata.md b/docs/architecture-metadata.md new file mode 100644 index 000000000..e69de29bb From 1280b16bcdb1378579a0e0155856426664815442 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 13 Jul 2017 20:47:30 +0700 Subject: [PATCH 11/47] Remove some global css, fix some styling issues --- client/coral-embed-stream/src/components/Stream.css | 3 +++ client/coral-embed-stream/style/default.css | 12 ------------ client/coral-plugin-best/BestButton.css | 4 ++++ client/coral-plugin-best/BestButton.js | 2 +- client/coral-ui/components/Icon.css | 7 +++++++ client/coral-ui/components/Icon.js | 4 +++- 6 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 client/coral-ui/components/Icon.css diff --git a/client/coral-embed-stream/src/components/Stream.css b/client/coral-embed-stream/src/components/Stream.css index 06f1efd1f..8c5a4250b 100644 --- a/client/coral-embed-stream/src/components/Stream.css +++ b/client/coral-embed-stream/src/components/Stream.css @@ -22,5 +22,8 @@ } .tabContainer { + position: relative; margin-top: 28px; + padding-bottom: 50px; + min-height: 600px; } diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 887b780ce..437397a98 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -21,18 +21,6 @@ body { padding: 4px; } -.talk-stream-tab-container { - padding-bottom: 50px; - min-height: 600px; -} - -.talk-stream-tab-container .material-icons { - vertical-align: middle; - width: 1em; - font-size: 1em; - overflow: hidden; -} - .expandForSignin { min-height: 600px; } diff --git a/client/coral-plugin-best/BestButton.css b/client/coral-plugin-best/BestButton.css index b1f169ff6..d1660ad5a 100644 --- a/client/coral-plugin-best/BestButton.css +++ b/client/coral-plugin-best/BestButton.css @@ -2,3 +2,7 @@ composes: buttonReset from "coral-framework/styles/reset.css"; margin: 5px 10px 5px 0px; } + +.tagIcon { + font-size: 12px; +} diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index ac6d03fd8..c5ebeb907 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -18,7 +18,7 @@ const canModifyBestTag = ({roles = []} = {}) => roles && ['ADMIN', 'MODERATOR']. // Put this on a comment to show that it is best -export const BestIndicator = ({children = }) => ( +export const BestIndicator = ({children = }) => ( { children } diff --git a/client/coral-ui/components/Icon.css b/client/coral-ui/components/Icon.css new file mode 100644 index 000000000..ec9a9dabc --- /dev/null +++ b/client/coral-ui/components/Icon.css @@ -0,0 +1,7 @@ + +.root { + vertical-align: middle; + width: 1em; + font-size: 1em; + overflow: hidden; +} diff --git a/client/coral-ui/components/Icon.js b/client/coral-ui/components/Icon.js index b7c49a799..025b6df9e 100644 --- a/client/coral-ui/components/Icon.js +++ b/client/coral-ui/components/Icon.js @@ -1,8 +1,10 @@ import React, {PropTypes} from 'react'; import {Icon as IconMDL} from 'react-mdl'; +import cn from 'classnames'; +import styles from './Icon.css'; const Icon = ({className = '', name}) => ( - + ); Icon.propTypes = { From 47fd522f68553aac209f2c4b87581a1a27ae47d2 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Thu, 13 Jul 2017 10:25:36 -0400 Subject: [PATCH 12/47] Add first draft --- docs/architecture-metadata.md | 94 +++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/architecture-metadata.md b/docs/architecture-metadata.md index e69de29bb..73530edf3 100644 --- a/docs/architecture-metadata.md +++ b/docs/architecture-metadata.md @@ -0,0 +1,94 @@ +--- +title: Metadata API +keywords: architecture +sidebar: talk_sidebar +permalink: architecture-metadata.html +summary: +--- + +The _metadata api_ allows you to add fields to models that are not represented in the core schema. Most core models ship with metadata enabled. If you would like to add metadata to a model that does not support it, [please let us know](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md#writing-code). + +## Goals + +The metadata api is designed to satisfy two product goals: + +* Give developers flexibility in extending datatypes. +* Protect core fields that are essential to Talk's operation. + +## Design + +Metadata is represented by an [subdocument in our Schemas](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/models/comment.js#L74). This takes advantage of Mongo's flexibility allowing for any data to be stored therein. + +### Setting Metadata + +Talk provides [a service layer](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js) allowing developer to `set` and `unset` metadata on objects in a way similar to key-value stores. + +Let's say that I want to add a custom field called `potency` to a comment. + +``` +const MetadataService = require('services/metadata'); +const CommentModel = require('models/comment'); + +// Sets the property `loaded` on the comment with `id=1`. +MetadataService.set(CommentModel, '1', 'potency', 42); +``` + +Note that the model passed here is the Model itself and not a loaded comment. This allows us to update the value on that document [in an atomic manner](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js#L60) for efficiency and to prevent race conditions. + +### Accessing Metadata + +The metadata api does not contain a `get` method. This is so because the metadata object is retrieved through database queries. It is up to the code that accesses the objects from the database to handle relevant metadata fields. + +The metadata object can be queried as any other subdocument without restriction. + +#### Accessing via the Graph + +One of the first principles of GraphQL is that the shape of the graph does not need to be the same as the shape of the data in the database. In fact, it probably shouldn't be. + +This enables us to treat metadata fields in any way that makes sense as we design our Graph. The fact that a value is stored in the metadata object is an implementation detail invisible to the front end. + +Take for example, the `reason` field in the `FlagAction` type. This stores the user provided reason that they flagged a comment. As far as the front end knows, it's [just another field](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/graph/typeDefs.graphql#L453) alongside the core fields: + +``` +# graph/typeDefs.graphql +type FlagAction implements Action { + + ... + + # The reason for which the Flag Action was created. + reason: String + + ... +} +``` + +If, however, we [look at the resolver](https://github.com/coralproject/talk/blob/a47e2378e96f34f25447782f3e7ce59fa48ec791/graph/resolvers/dont_agree_action.js) for that field, we see that `message` is destructured from the metadata object and returned. + +``` +// graph/resolvers/dont_agree_action.js +const DontAgreeAction = { + + // Stored in the metadata, extract and return. + reason({metadata: {reason}}) { + return reason; + } +}; + +module.exports = DontAgreeAction; +``` + +This is an extremely powerful pattern as it allows us absolute freedom in designing our graph and complete isolation of the added values in the database. + +## Some things to keep in mind + +### Namespace your metadata fields + +Since metadata can be added by the core and multiple plugins, collisions may occur. As you create your plugins, please be careful to pick unique names for metadata fields. We recommend namespacing all your fields in a subdocument named after your plugin. + +``` +[model].metadata.[your_plugin_name].[the_field] +``` + +### Querying by metadata fields + +We currently do not have a clean way to index metadata fields. As a result queries that match against metadata fields will not scale. If you have a need to match, sort, etc... by a metadata field, [please let us know](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md#writing-code). From 83832abd726f52891f025c86abb8a7ab3961f8ae Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 13 Jul 2017 21:37:08 +0700 Subject: [PATCH 13/47] More fixes --- client/coral-embed-stream/src/components/Comment.css | 5 +++++ .../src/components/EditableCommentContent.js | 2 +- client/coral-embed-stream/style/default.css | 5 ----- client/coral-plugin-best/BestButton.css | 6 ++++++ client/coral-plugin-best/BestButton.js | 2 +- client/coral-plugin-flags/components/FlagButton.js | 2 +- client/coral-plugin-flags/components/styles.css | 6 ++++++ client/coral-plugin-history/Comment.css | 5 +++++ client/coral-plugin-history/Comment.js | 4 ++-- client/coral-plugin-replies/ReplyButton.css | 6 ++++++ client/coral-plugin-replies/ReplyButton.js | 2 +- client/coral-ui/components/Icon.css | 4 ---- plugins/coral-plugin-like/client/styles.css | 4 +++- plugins/coral-plugin-love/client/styles.css | 4 +++- plugins/coral-plugin-respect/client/styles.css | 3 ++- .../client/components/ViewingOptions.css | 5 +++++ .../client/components/ViewingOptions.js | 2 +- .../client/components/PermalinkButton.js | 2 +- plugins/talk-plugin-permalink/client/components/styles.css | 6 ++++++ 19 files changed, 55 insertions(+), 20 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index d397185dc..143577cf6 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -157,3 +157,8 @@ .enter { animation: enter 1000ms; } + +.timerIcon { + vertical-align: middle; + font-size: 14px; +} diff --git a/client/coral-embed-stream/src/components/EditableCommentContent.js b/client/coral-embed-stream/src/components/EditableCommentContent.js index 06d8e6b9d..98be14d13 100644 --- a/client/coral-embed-stream/src/components/EditableCommentContent.js +++ b/client/coral-embed-stream/src/components/EditableCommentContent.js @@ -145,7 +145,7 @@ export class EditableCommentContent extends React.Component { } : - {t('edit_comment.edit_window_timer_prefix')} + {t('edit_comment.edit_window_timer_prefix')} (remainingMs <= 10 * 1000) ? styles.editWindowAlmostOver : '' } diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 437397a98..90a6250a3 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -271,11 +271,6 @@ body { float: left; } -.comment__action-container .material-icons { - font-size: 12px; - margin-left: 3px; -} - button.comment__action-button, .comment__action-button button { cursor: pointer; diff --git a/client/coral-plugin-best/BestButton.css b/client/coral-plugin-best/BestButton.css index d1660ad5a..ca77f01f2 100644 --- a/client/coral-plugin-best/BestButton.css +++ b/client/coral-plugin-best/BestButton.css @@ -5,4 +5,10 @@ .tagIcon { font-size: 12px; + vertical-align: middle; +} + +.icon { + font-size: 12px; + vertical-align: middle; } diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index c5ebeb907..34bdad201 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -98,7 +98,7 @@ export class BestButton extends Component { disabled={disabled} className={cn(styles.button, `${name}-button`, `e2e__${isBest ? 'unset' : 'set'}-best-comment`)} aria-label={t(isBest ? 'unset_best' : 'set_best')}> - + ); } diff --git a/client/coral-plugin-flags/components/FlagButton.js b/client/coral-plugin-flags/components/FlagButton.js index 33499e83d..51b88391e 100644 --- a/client/coral-plugin-flags/components/FlagButton.js +++ b/client/coral-plugin-flags/components/FlagButton.js @@ -155,7 +155,7 @@ export default class FlagButton extends Component { : {t('report')} } {
  • - {t('view_conversation')} + {t('view_conversation')}
  • - + { {t('reply')} - reply ); diff --git a/client/coral-ui/components/Icon.css b/client/coral-ui/components/Icon.css index ec9a9dabc..16fe6d235 100644 --- a/client/coral-ui/components/Icon.css +++ b/client/coral-ui/components/Icon.css @@ -1,7 +1,3 @@ .root { - vertical-align: middle; - width: 1em; - font-size: 1em; - overflow: hidden; } diff --git a/plugins/coral-plugin-like/client/styles.css b/plugins/coral-plugin-like/client/styles.css index 9ce0d5271..859581bd4 100644 --- a/plugins/coral-plugin-like/client/styles.css +++ b/plugins/coral-plugin-like/client/styles.css @@ -25,7 +25,9 @@ } .icon { - padding: 0 2px; + font-size: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; } @media (max-width: 425px) { diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css index d7753a0e9..9d1bec487 100644 --- a/plugins/coral-plugin-love/client/styles.css +++ b/plugins/coral-plugin-love/client/styles.css @@ -25,7 +25,9 @@ } .icon { - padding: 0 2px; + font-size: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; } @media (max-width: 425px) { diff --git a/plugins/coral-plugin-respect/client/styles.css b/plugins/coral-plugin-respect/client/styles.css index 3ab0fbd29..362e02b8f 100644 --- a/plugins/coral-plugin-respect/client/styles.css +++ b/plugins/coral-plugin-respect/client/styles.css @@ -26,7 +26,8 @@ } .icon { - padding: 0 2px; + font-size: 12px; + padding: 0 3px; } @media (max-width: 425px) { diff --git a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css index 9c302d07e..81d373b95 100644 --- a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css +++ b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css @@ -24,3 +24,8 @@ list-style: none; white-space: nowrap; } + +.icon { + font-size: 14px; + vertical-align: middle; +} diff --git a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js index b64123fdb..87680b370 100644 --- a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.js @@ -24,7 +24,7 @@ const ViewingOptions = (props) => {
    { diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js index 55b98e5d8..cca8718b5 100644 --- a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js +++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js @@ -72,7 +72,7 @@ export default class PermalinkButton extends React.Component { onClick={this.toggle} className={cn(`${name}-button`, styles.button)}> {t('permalink')} - +
    Date: Thu, 13 Jul 2017 11:37:54 -0300 Subject: [PATCH 14/47] Support for empty slot --- client/coral-embed-stream/src/components/Comment.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index c9dbeac69..bb06f7f1e 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -165,7 +165,9 @@ } .commentAvatar { - max-width: 50px; - margin-right: 10px; - margin-top: 10px; + max-width: 60px; +} + +.commentAvatar:empty { + display: none; } From dc76d89c47394a4588578b15f753f533584b1c8e Mon Sep 17 00:00:00 2001 From: David Erwin Date: Thu, 13 Jul 2017 10:49:38 -0400 Subject: [PATCH 15/47] Fewer, better words --- docs/_data/sidebars/talk_sidebar.yml | 32 ++++++++++++++-------------- docs/architecture-metadata.md | 16 ++++++-------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/docs/_data/sidebars/talk_sidebar.yml b/docs/_data/sidebars/talk_sidebar.yml index ebbc6e006..282defcb5 100644 --- a/docs/_data/sidebars/talk_sidebar.yml +++ b/docs/_data/sidebars/talk_sidebar.yml @@ -37,6 +37,22 @@ entries: url: /install-setup.html output: web + - title: Architecture + output: web + folderitems: + - title: Overview + url: /architecture.html + output: web + - title: Tags + url: /architecture-tags.html + output: web + - title: Metadata API + url: /architecture-metadata.html + output: web + - title: cli + url: /architecture-cli.html + output: web + - title: Plugins output: web folderitems: @@ -56,22 +72,6 @@ entries: url: /plugins-experimental.html output: web - - title: Architecture - output: web - folderitems: - - title: Overview - url: /architecture.html - output: web - - title: Tags - url: /architecture-tags.html - output: web - - title: Metadata API - url: /architecture-metadata.html - output: web - - title: cli - url: /architecture-cli.html - output: web - - title: Development output: web folderitems: diff --git a/docs/architecture-metadata.md b/docs/architecture-metadata.md index 73530edf3..043a907eb 100644 --- a/docs/architecture-metadata.md +++ b/docs/architecture-metadata.md @@ -1,12 +1,12 @@ --- -title: Metadata API +title: Metadata keywords: architecture sidebar: talk_sidebar permalink: architecture-metadata.html summary: --- -The _metadata api_ allows you to add fields to models that are not represented in the core schema. Most core models ship with metadata enabled. If you would like to add metadata to a model that does not support it, [please let us know](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md#writing-code). +_Metadata_ allows you to add fields to models that are not represented in the core schema. ## Goals @@ -29,19 +29,17 @@ Let's say that I want to add a custom field called `potency` to a comment. const MetadataService = require('services/metadata'); const CommentModel = require('models/comment'); -// Sets the property `loaded` on the comment with `id=1`. +// Sets the property `potency` on the comment with `id=1`. MetadataService.set(CommentModel, '1', 'potency', 42); ``` -Note that the model passed here is the Model itself and not a loaded comment. This allows us to update the value on that document [in an atomic manner](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js#L60) for efficiency and to prevent race conditions. +Note that the model passed here is the Model itself and not an individual comment object. This allows us to update the value on that document [in an atomic manner](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js#L60) for efficiency and to prevent race conditions. ### Accessing Metadata -The metadata api does not contain a `get` method. This is so because the metadata object is retrieved through database queries. It is up to the code that accesses the objects from the database to handle relevant metadata fields. +The metadata api does not contain a `get` method. The metadata object is retrieved via database queries along with the rest of the data. -The metadata object can be queried as any other subdocument without restriction. - -#### Accessing via the Graph +## Metadata and the Graph One of the first principles of GraphQL is that the shape of the graph does not need to be the same as the shape of the data in the database. In fact, it probably shouldn't be. @@ -62,7 +60,7 @@ type FlagAction implements Action { } ``` -If, however, we [look at the resolver](https://github.com/coralproject/talk/blob/a47e2378e96f34f25447782f3e7ce59fa48ec791/graph/resolvers/dont_agree_action.js) for that field, we see that `message` is destructured from the metadata object and returned. +If, however, we [look at the resolver](https://github.com/coralproject/talk/blob/a47e2378e96f34f25447782f3e7ce59fa48ec791/graph/resolvers/dont_agree_action.js) for that field, we see that `message` is [destructured](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) from the metadata object and returned. ``` // graph/resolvers/dont_agree_action.js From 30b200d7346434c2e755e678748a84b7a3bf08ab Mon Sep 17 00:00:00 2001 From: David Erwin Date: Thu, 13 Jul 2017 10:58:31 -0400 Subject: [PATCH 16/47] Fix typos --- docs/architecture-metadata.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/architecture-metadata.md b/docs/architecture-metadata.md index 043a907eb..089127343 100644 --- a/docs/architecture-metadata.md +++ b/docs/architecture-metadata.md @@ -21,7 +21,7 @@ Metadata is represented by an [subdocument in our Schemas](https://github.com/co ### Setting Metadata -Talk provides [a service layer](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js) allowing developer to `set` and `unset` metadata on objects in a way similar to key-value stores. +Talk provides [a service layer](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/services/metadata.js) allowing developers to `set` and `unset` metadata on objects in a way similar to key-value stores. Let's say that I want to add a custom field called `potency` to a comment. @@ -45,7 +45,7 @@ One of the first principles of GraphQL is that the shape of the graph does not n This enables us to treat metadata fields in any way that makes sense as we design our Graph. The fact that a value is stored in the metadata object is an implementation detail invisible to the front end. -Take for example, the `reason` field in the `FlagAction` type. This stores the user provided reason that they flagged a comment. As far as the front end knows, it's [just another field](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/graph/typeDefs.graphql#L453) alongside the core fields: +Take for example, the `reason` field in the `FlagAction` type. This stores the user provided reason why they flagged a comment. As far as the front end knows, it's [just another field](https://github.com/coralproject/talk/blob/c59c09e1f42c51eed3b0d57b7c2882fc7b5edc13/graph/typeDefs.graphql#L453) alongside the core fields: ``` # graph/typeDefs.graphql @@ -60,7 +60,7 @@ type FlagAction implements Action { } ``` -If, however, we [look at the resolver](https://github.com/coralproject/talk/blob/a47e2378e96f34f25447782f3e7ce59fa48ec791/graph/resolvers/dont_agree_action.js) for that field, we see that `message` is [destructured](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) from the metadata object and returned. +If, however, we [look at the resolver](https://github.com/coralproject/talk/blob/a47e2378e96f34f25447782f3e7ce59fa48ec791/graph/resolvers/dont_agree_action.js) for that field, we see that `reason` is [destructured](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) from the metadata object and returned. ``` // graph/resolvers/dont_agree_action.js @@ -75,7 +75,7 @@ const DontAgreeAction = { module.exports = DontAgreeAction; ``` -This is an extremely powerful pattern as it allows us absolute freedom in designing our graph and complete isolation of the added values in the database. +This is an extremely powerful pattern as it allows us absolute freedom in designing our graph and complete isolation of the added fields in the database. ## Some things to keep in mind From 1541a618b594f4d29639c6f2ebfd824862774c56 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 13 Jul 2017 12:29:06 -0300 Subject: [PATCH 17/47] Reordering divs --- client/coral-embed-stream/src/components/Comment.css | 12 ++++++------ client/coral-embed-stream/src/components/Comment.js | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index bb06f7f1e..48a848848 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -19,8 +19,9 @@ } .comment { - padding-left: 20px; - flex: auto; + padding-left: 15px; + display: flex; + flex-flow: row; } .commentLevel0 { @@ -44,7 +45,7 @@ } .highlightedComment { - padding-left: 20px; + padding-left: 15px; border-left: 3px solid rgb(35,118,216); } @@ -159,9 +160,8 @@ animation: enter 1000ms; } -.commentRow { - display: flex; - flex-flow: row; +.commentContainer { + flex: auto; } .commentAvatar { diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index e612affb0..9e6a70efe 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -429,7 +429,8 @@ export default class Comment extends React.Component { className={rootClassName} id={`c_${comment.id}`} > -
    +
    + -
    +
    + {isStaff(comment.tags) ? Staff : null} From b79c990ee57a25b54ff937e6c8e79d885fbb22d3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 13 Jul 2017 22:30:10 +0700 Subject: [PATCH 18/47] Only setState once --- client/coral-plugin-commentbox/CommentBox.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 6e17b119f..084e67fc4 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -67,7 +67,7 @@ class CommentBox extends React.Component { postComment(comment, 'comments') .then(({data}) => { - this.setState({loadingState: 'success'}); + this.setState({loadingState: 'success', body: ''}); const postedComment = data.createComment.comment; // Execute postSubmit Hooks @@ -78,8 +78,6 @@ class CommentBox extends React.Component { if (commentPostedHandler) { commentPostedHandler(); } - - this.setState({body: ''}); }) .catch((err) => { this.setState({loadingState: 'error'}); From 28e94d30b094c637741c207deb551670f91a6ba2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 13 Jul 2017 23:00:52 +0700 Subject: [PATCH 19/47] Only show seperator on top-level comments --- client/coral-embed-stream/src/components/Comment.css | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 72cf25b21..9a176534d 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -2,20 +2,17 @@ margin-left: 20px; margin-bottom: 16px; position: relative; - border-top: 1px solid rgba(0, 0, 0, 0.1); padding-top: 12px; } .rootLevel0:first-child { padding-top: 0; -} - -.root:first-child { border: 0; } .rootLevel0 { margin-left: 0px; + border-top: 1px solid rgba(0, 0, 0, 0.1); } .comment { From 7b53048a91f99ca17027e581a6bbbd4a3f72b20a Mon Sep 17 00:00:00 2001 From: David Erwin Date: Thu, 13 Jul 2017 12:04:18 -0400 Subject: [PATCH 20/47] Add install troubleshooting doc --- docs/_data/sidebars/talk_sidebar.yml | 7 ++++++- docs/install-troubleshooting.md | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/install-troubleshooting.md diff --git a/docs/_data/sidebars/talk_sidebar.yml b/docs/_data/sidebars/talk_sidebar.yml index 62660783c..02cac9f3f 100644 --- a/docs/_data/sidebars/talk_sidebar.yml +++ b/docs/_data/sidebars/talk_sidebar.yml @@ -36,6 +36,9 @@ entries: - title: Setup url: /install-setup.html output: web + - title: Troubleshooting + url: /install-troubleshooting.html + output: web - title: Architecture output: web @@ -46,11 +49,13 @@ entries: - title: Tags url: /architecture-tags.html output: web + - title: Metadata API + url: /architecture-metadata.html + output: web - title: cli url: /architecture-cli.html output: web - - title: Plugins output: web folderitems: diff --git a/docs/install-troubleshooting.md b/docs/install-troubleshooting.md new file mode 100644 index 000000000..6ff42af05 --- /dev/null +++ b/docs/install-troubleshooting.md @@ -0,0 +1,22 @@ +--- +title: Installation Troubleshooting +keywords: install +sidebar: talk_sidebar +permalink: install-troubleshooting.html +summary: +--- + +This page tracks common issues that arise when installing Talk and provides resolutions. + +## The Talk server seems to be working but the stream isn't showing up on my page. + +Talk employs a _domain whitelist_ that controls which sites can contain comment threads. This prevents malicious folks from using your server to embed streams on unwanted pages. + +If your comment thread isn't showing: + +1. Log into your admin panel +1. Go to the Configure tab +1. Select the Tech Settings submenu +1. Ensure that your Domain is the Permitted Domains list + +Note: if your site has multiple subdomains, listing the domain itself (ie: `mydomain.com`) will enable Talk on all subdomains. If you would like to restrict Talk to certain subdomains, you must list all of them here (ie: `thisone.mydomain.com thatone.mydomain.com`). From ea6330bf945198fa80e67be2651f6f098da4b5af Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 13 Jul 2017 13:01:47 -0400 Subject: [PATCH 21/47] Update README with info about Recipes --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 68547e7a8..a629065ff 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ See our [Talk Documentation & Guides](https://coralproject.github.io/talk/index. See our guide to using and building [Talk Plugins](https://github.com/coralproject/talk/blob/master/PLUGINS.md). +### Recipes + +Recipes are plugin templates provided by the Coral Core team. Developers can use these recipes to build their own plugins. You can find all the Talk recipes here: https://github.com/coralproject/talk-recipes/ + ## Usage ### Installation From 31cfddf93cc83e4cacb3e4f39f976f9770764199 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 13 Jul 2017 14:20:26 -0400 Subject: [PATCH 22/47] Remove "like" from default plugins --- plugins.default.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins.default.json b/plugins.default.json index 3bc2e41bf..425dccf93 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -1,14 +1,12 @@ { "server": [ "coral-plugin-auth", - "coral-plugin-like", "coral-plugin-respect", "coral-plugin-offtopic", "coral-plugin-facebook-auth" ], "client": [ "coral-plugin-respect", - "coral-plugin-like", "coral-plugin-auth", "coral-plugin-offtopic", "coral-plugin-viewing-options", From e8bbd7e4d7a8a5b27e41981ab4f1bef42d12ca32 Mon Sep 17 00:00:00 2001 From: Andrew Losowsky Date: Fri, 14 Jul 2017 10:34:58 -0400 Subject: [PATCH 23/47] Created Code of Conduct Merged ours with the Contributor Covenant --- CODE_OF_CONDUCT.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..751aa263d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +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. + + +## 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, 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. + +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. From d4cb07e4487f817bd8b91c4cb26d480685ab4fd0 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 14 Jul 2017 12:39:48 -0400 Subject: [PATCH 24/47] Update node version in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5c7b3cabb..3211dfd60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:7.8 +FROM node:8.1.4 # Create app directory RUN mkdir -p /usr/src/app From 8cbb56af19f374e4271e2a266e8a8f189f6a4e58 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 14 Jul 2017 12:40:39 -0400 Subject: [PATCH 25/47] Update default node version for Talk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71a07f4da..1c788bb30 100644 --- a/package.json +++ b/package.json @@ -214,6 +214,6 @@ "webpack": "^2.3.1" }, "engines": { - "node": "^7.8.0" + "node": "^8.1.4" } } From 04d330cf5bf1576be442464b0ae3bcde6975d628 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 14 Jul 2017 12:41:15 -0400 Subject: [PATCH 26/47] Update node version in CircleCI config --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index b924028f3..fd959ea61 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 7.9 + version: 8.1.4 services: - docker - redis From c610c319dec910adc85029d96f85cf08bcc96f88 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 14 Jul 2017 13:16:38 -0400 Subject: [PATCH 27/47] Downgrade to 7.10.1 due to dependency issues --- Dockerfile | 2 +- circle.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3211dfd60..1f67da95b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8.1.4 +FROM node:7.10.1 # Create app directory RUN mkdir -p /usr/src/app diff --git a/circle.yml b/circle.yml index fd959ea61..4e695edcc 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 8.1.4 + version: 7.10 services: - docker - redis diff --git a/package.json b/package.json index 1c788bb30..76b0c299a 100644 --- a/package.json +++ b/package.json @@ -214,6 +214,6 @@ "webpack": "^2.3.1" }, "engines": { - "node": "^8.1.4" + "node": "^7.10.1" } } From 05e2302794193374377ccf174782565db81bc747 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 14 Jul 2017 13:31:35 -0400 Subject: [PATCH 28/47] Update CircleCI node version --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 4e695edcc..69f95c9b4 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 7.10 + version: 7.10.1 services: - docker - redis From acb7028471b39cb7fddc93d5633e17a32fddfca3 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Fri, 14 Jul 2017 16:01:57 -0400 Subject: [PATCH 29/47] Write microservice docs --- docs/_data/sidebars/talk_sidebar.yml | 3 ++ docs/install-microservices.md | 63 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 docs/install-microservices.md diff --git a/docs/_data/sidebars/talk_sidebar.yml b/docs/_data/sidebars/talk_sidebar.yml index 02cac9f3f..b483b47d1 100644 --- a/docs/_data/sidebars/talk_sidebar.yml +++ b/docs/_data/sidebars/talk_sidebar.yml @@ -36,6 +36,9 @@ entries: - title: Setup url: /install-setup.html output: web + - title: Microservice Deployments + url: /install-microservices.html + output: web - title: Troubleshooting url: /install-troubleshooting.html output: web diff --git a/docs/install-microservices.md b/docs/install-microservices.md new file mode 100644 index 000000000..4100c4cac --- /dev/null +++ b/docs/install-microservices.md @@ -0,0 +1,63 @@ +--- +title: Microservice Deployments +keywords: install +sidebar: talk_sidebar +permalink: install-microservices.html +summary: +--- + +The Talk server serves several logically/architecturally distinct functions: + +* A web server that + * serves "public" assets (aka, the comment embed) + * "protected" assets (aka, the admin console) over http(s) + * the GraphQL endpoints +* A web socket server that handles subscriptions. +* A jobs processor that handles queued operations. + +In the documentation so far, we've discussed how to deploy all of these functionalities bundled into a single monolith application. This is convenient as there is minimal configuration and horizontal scaling is as easy as upping the number of servers behind a single load balancer. + +## Separating Talk into Microservices + +Talk can be run as three separate clusters of servers: webserver, socket server and jobs server. + +Note that the `cli serve` command, which is responsible for starting the server, contains flags that control whether `jobs` and `websockets` are enabled. + +``` +talk :) ]$ bin/cli serve --help + + Options: + + ... + -j, --jobs enable job processing on this thread + -w, --websockets enable the websocket (subscriptions) handler on this thread + ... +``` + +Each Talk Microservice cluster can be deployed in an identical manner described in the other docs in this section with the omission of the `-j` and/or `-w` flags. + +With routing logic in front of the webserver cluster, separation between public and protected assets can be achieved. + +## When should I consider separating? + +Consider a microservice deployment if: + +* you want to put access to admin routes behind a firewall +* you are running plugins that require intensive job processing +* you do not want to simplicity of single cluster horizontal scaling and want to tune the economy and performance of your install. + +At scale, combining separate concerns in a single process makes it very difficult to understand what is taking up resources. With microservices, each server could be configured to sit behind it's own load balance and scale independently. Each variety of process can always have just enough resources. + +An install that heavily utilizes the jobs queue could see delays in http service because of heavy jobs processes and/or delays in the execution of jobs processes due to increased server load. + +## Deployment Methodologies + +Note that there is no flag to separate the http routes on the webserver. Separating the http server functionalities can be accomplished by the routing of various routes to the correct http server. This can ensure that sensitive areas, such as the `/admin/` route are not available outside the firewall. + +Talk's Queue is backed by Redis, so as long as all Talk instances access the same Redis cluster no additional configuration is needed when launching an independent jobs cluster. + +If there are any features of Talk that you believe should be disable-able via server flags, please let us know and consider contributing it to the project! + +## Deployment Flows/Scripts + +We do not currently support any microservice based deployment flows. If you develop one yourself that is completely based on open source tooling, please consider contributing it to the project! From b235b8334e530eec2cd746ad4e4361eefb748594 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Fri, 14 Jul 2017 16:06:47 -0400 Subject: [PATCH 30/47] Give some context --- docs/install-microservices.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/install-microservices.md b/docs/install-microservices.md index 4100c4cac..c3215f730 100644 --- a/docs/install-microservices.md +++ b/docs/install-microservices.md @@ -6,6 +6,12 @@ permalink: install-microservices.html summary: --- +In Talk, we seek to deliver the simplicity of a monolith with the advantages of a microservice based infrastructure for those who want them. + +To accomplish this, Talk has the ability to run with subsets of its overall functionality and contains architecture that allows them to operate logically as microservices when running in a single environment. + +## Talk Server Functionalities + The Talk server serves several logically/architecturally distinct functions: * A web server that @@ -19,7 +25,9 @@ In the documentation so far, we've discussed how to deploy all of these function ## Separating Talk into Microservices -Talk can be run as three separate clusters of servers: webserver, socket server and jobs server. +Talk can be run as three separate clusters of servers by enabling/disabling different bits of functionality: webserver, socket server and jobs server. + +Each microservice would deploy with the same codebase and configuration. Note that the `cli serve` command, which is responsible for starting the server, contains flags that control whether `jobs` and `websockets` are enabled. From 0069f31244a317ed98efb391ffc53dec028de1f0 Mon Sep 17 00:00:00 2001 From: David Erwin Date: Fri, 14 Jul 2017 16:07:47 -0400 Subject: [PATCH 31/47] Make it accurate --- docs/install-microservices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install-microservices.md b/docs/install-microservices.md index c3215f730..eb3111c2d 100644 --- a/docs/install-microservices.md +++ b/docs/install-microservices.md @@ -25,7 +25,7 @@ In the documentation so far, we've discussed how to deploy all of these function ## Separating Talk into Microservices -Talk can be run as three separate clusters of servers by enabling/disabling different bits of functionality: webserver, socket server and jobs server. +Talk can be run in two or more separate clusters of servers by enabling/disabling different bits of functionality: webserver, socket server and jobs server. Each microservice would deploy with the same codebase and configuration. From fb2662572ad4b59a4145f8ec64d0f3bfbe8bc01c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 14 Jul 2017 22:43:47 +0700 Subject: [PATCH 32/47] Implement excludeIf for the plugins API --- .../src/components/Embed.js | 16 +- .../src/components/Stream.js | 14 +- .../src/containers/Embed.js | 5 +- .../src/containers/Stream.js | 3 +- client/coral-framework/actions/auth.js | 54 +++---- .../components/IfSlotIsEmpty.js | 22 +++ .../components/IfSlotIsNotEmpty.js | 22 +++ client/coral-framework/components/Popup.js | 152 ++++++++++++++++++ client/coral-framework/components/Slot.js | 8 +- client/coral-framework/constants/auth.js | 2 + client/coral-framework/helpers/plugins.js | 21 ++- client/coral-framework/hocs/excludeIf.js | 4 + client/coral-framework/hocs/withFragments.js | 15 +- client/coral-framework/reducers/auth.js | 11 +- plugin-api/beta/client/hocs/index.js | 1 + plugin-api/beta/client/selectors/index.js | 1 + .../client/containers/Tab.js | 2 + 17 files changed, 281 insertions(+), 72 deletions(-) create mode 100644 client/coral-framework/components/IfSlotIsEmpty.js create mode 100644 client/coral-framework/components/IfSlotIsNotEmpty.js create mode 100644 client/coral-framework/components/Popup.js create mode 100644 client/coral-framework/hocs/excludeIf.js create mode 100644 plugin-api/beta/client/selectors/index.js diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index fee6fe025..a069984dc 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -6,6 +6,8 @@ import t from 'coral-framework/services/i18n'; import {TabBar, Tab, TabContent, TabPane} from 'coral-ui'; import ProfileContainer from 'coral-settings/containers/ProfileContainer'; +import Popup from 'coral-framework/components/Popup'; +import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty'; import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; import cn from 'classnames'; @@ -26,12 +28,24 @@ export default class Embed extends React.Component { }; render() { - const {activeTab, commentId} = this.props; + const {activeTab, commentId, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props; const {user} = this.props.auth; const hasHighlightedComment = !!commentId; return (
    + + +
    - {getSlotComponents('streamTabs').map((PluginComponent) => ( + {getSlotComponents('streamTabs', pluginConfig, streamTabProps).map((PluginComponent) => ( ))} @@ -222,12 +222,10 @@ class Stream extends React.Component { - {getSlotComponents('streamTabPanes').map((PluginComponent) => ( + {getSlotComponents('streamTabPanes', pluginConfig, streamTabProps).map((PluginComponent) => ( ))} diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index a5c6ba1a6..aa0700ad5 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -19,7 +19,7 @@ import t from 'coral-framework/services/i18n'; import {setActiveTab} from '../actions/embed'; -const {logout, checkLogin} = authActions; +const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions; const {fetchAssetSuccess} = assetActions; class EmbedContainer extends React.Component { @@ -185,6 +185,9 @@ const mapDispatchToProps = (dispatch) => setActiveTab, fetchAssetSuccess, addNotification, + focusSignInDialog, + blurSignInDialog, + hideSignInDialog, }, dispatch ); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 994d749f3..4b4a0e105 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -310,7 +310,8 @@ const mapStateToProps = (state) => ({ previousTab: state.embed.previousTab, activeStreamTab: state.stream.activeTab, previousStreamTab: state.stream.previousTab, - commentClassNames: state.stream.commentClassNames + commentClassNames: state.stream.commentClassNames, + pluginConfig: state.config.plugin_config, }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 732a11ebe..d3c29dc3c 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -7,45 +7,33 @@ import pym from '../services/pym'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; -import {isSlotEmpty} from 'plugin-api/beta/client/services'; -export const showSignInDialog = () => (dispatch, getState) => { - if (isSlotEmpty('login')) { - return; - } - const signInPopUp = window.open( - '/embed/stream/login', - 'Login', - 'menubar=0,resizable=0,width=500,height=550,top=200,left=500' - ); +export const showSignInDialog = () => ({ + type: actions.SHOW_SIGNIN_DIALOG, +}); - // Workaround odd behavior in older WebKit versions, where - // onunload is called twice. (Encountered in IOS 8.3) - let loaded = false; - signInPopUp.onload = () => { - loaded = true; - - // Fire some actions inside the popups reducer, to initialize required state. - const required = getState().asset.toJS().settings.requireEmailConfirmation; - const redirectUri = getState().auth.toJS().redirectUri; - signInPopUp.coralStore.dispatch(setRequireEmailVerification(required)); - signInPopUp.coralStore.dispatch(setRedirectUri(redirectUri)); - }; - - // Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari. - signInPopUp.onunload = () => { - if (loaded) { - dispatch(checkLogin()); - } - }; - - dispatch({type: actions.SHOW_SIGNIN_DIALOG}); -}; export const hideSignInDialog = () => (dispatch) => { + if (window.opener && window.opener !== window) { + + // TODO: We need to address this when we refactor the + // login popup out of the embed. + + // we are in a popup + window.close(); + } else { + dispatch(checkLogin()); + } dispatch({type: actions.HIDE_SIGNIN_DIALOG}); - window.close(); }; +export const focusSignInDialog = () => ({ + type: actions.FOCUS_SIGNIN_DIALOG, +}); + +export const blurSignInDialog = () => ({ + type: actions.BLUR_SIGNIN_DIALOG, +}); + export const createUsernameRequest = () => ({ type: actions.CREATE_USERNAME_REQUEST }); diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js new file mode 100644 index 000000000..b4dacf1eb --- /dev/null +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {isSlotEmpty} from 'coral-framework/helpers/plugins'; +import PropTypes from 'prop-types'; + +function IfSlotIsEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) { + return ( + + {isSlotEmpty(slot, pluginConfig, rest) ? children : null} + + ); +} + +IfSlotIsEmpty.propTypes = { + slot: PropTypes.string, + className: PropTypes.string, +}; + +const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); + +export default connect(mapStateToProps, null)(IfSlotIsEmpty); + diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js new file mode 100644 index 000000000..820a528fe --- /dev/null +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {isSlotEmpty} from 'coral-framework/helpers/plugins'; +import PropTypes from 'prop-types'; + +function IfSlotIsNotEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) { + return ( + + {!isSlotEmpty(slot, pluginConfig, rest) ? children : null} + + ); +} + +IfSlotIsNotEmpty.propTypes = { + slot: PropTypes.string, + className: PropTypes.string, +}; + +const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); + +export default connect(mapStateToProps, null)(IfSlotIsNotEmpty); + diff --git a/client/coral-framework/components/Popup.js b/client/coral-framework/components/Popup.js new file mode 100644 index 000000000..b9393a36d --- /dev/null +++ b/client/coral-framework/components/Popup.js @@ -0,0 +1,152 @@ +import {Component} from 'react'; +import PropTypes from 'prop-types'; + +export default class Popup extends Component { + ref = null; + detectCloseInterval = null; + + constructor(props) { + super(props); + + if (props.open) { + this.openWindow(props); + } + } + + openWindow(props = this.props) { + this.ref = window.open( + props.href, + props.title, + props.features, + ); + + this.setCallbacks(); + } + + setCallbacks() { + this.ref.onload = () => { + clearInterval(this.detectCloseInterval); + this.onLoad(); + }; + + this.ref.onfocus = () => { + this.onFocus(); + }; + + this.ref.onblur = () => { + this.onBlur(); + }; + + // Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari. + this.ref.onunload = () => { + this.onUnload(); + + const interval = setInterval(() => { + if (this.ref.onload === null) { + this.setCallbacks(); + clearInterval(interval); + } + }, 50); + + this.detectCloseInterval = setInterval(() => { + if (this.ref.closed) { + clearInterval(this.detectCloseInterval); + this.onClose(); + } + }, 50); + }; + } + + closeWindow() { + if (this.ref) { + if (!this.ref.closed) { + this.ref.close(); + } + this.ref = null; + } + } + + focusWindow() { + if (this.ref && !this.ref.closed) { + this.ref.focus(); + } + } + + blurWindow() { + if (this.ref && !this.ref.closed) { + this.ref.blur(); + } + } + + onLoad = () => { + if (this.props.onLoad) { + this.props.onLoad(); + } + } + + onUnload = () => { + if (this.props.onUnload) { + this.props.onUnload(); + } + } + + onClose = () => { + if (this.props.onClose) { + this.props.onClose(); + } + } + + onFocus = () => { + if (this.props.onFocus) { + this.props.onFocus(); + } + } + + onBlur = () => { + if (this.props.onBlur) { + this.props.onBlur(); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.open && !this.ref) { + this.openWindow(nextProps); + } + + if (this.props.open && !nextProps.open) { + this.closeWindow(); + } + + if (!this.props.focus && nextProps.focus) { + this.focusWindow(); + } + + if (this.props.focus && !nextProps.focus) { + this.blurWindow(); + } + + if (this.props.href !== nextProps.href) { + this.ref.location.href = nextProps.href; + } + } + + componentWillUnmount() { + this.closeWindow(); + } + + render() { + return null; + } +} + +Popup.propTypes = { + open: PropTypes.bool, + focus: PropTypes.bool, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onLoad: PropTypes.func, + onUnload: PropTypes.func, + onClose: PropTypes.func, + href: PropTypes.string.isRequired, + features: PropTypes.string, +}; diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index ca246591b..2861a451c 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -4,14 +4,14 @@ import styles from './Slot.css'; import {connect} from 'react-redux'; import {getSlotElements} from 'coral-framework/helpers/plugins'; -function Slot ({fill, inline = false, className, plugin_config: config, defaultComponent: DefaultComponent, ...rest}) { - let children = getSlotElements(fill, {...rest, config}); +function Slot ({fill, inline = false, className, pluginConfig = {}, defaultComponent: DefaultComponent, ...rest}) { + let children = getSlotElements(fill, pluginConfig, rest); if (children.length === 0 && DefaultComponent) { children = ; } return ( -
    +
    {children}
    ); @@ -21,7 +21,7 @@ Slot.propTypes = { fill: React.PropTypes.string }; -const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config}); +const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); export default connect(mapStateToProps, null)(Slot); diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 57bde6f3f..324f75f44 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -3,6 +3,8 @@ export const CLEAN_STATE = 'CLEAN_STATE'; export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; +export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG'; +export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG'; export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST'; export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS'; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 625a0ee3f..55fdaf6c5 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -7,32 +7,31 @@ import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; import {loadTranslations} from 'coral-framework/services/i18n'; -import {injectReducers, getStore} from 'coral-framework/services/store'; +import {injectReducers} from 'coral-framework/services/store'; import camelize from './camelize'; -export function getSlotComponents(slot) { - const pluginConfig = getStore().getState().config.plugin_config; - +export function getSlotComponents(slot, pluginConfig, props = {}) { return flatten(plugins - // Filter out components that have slots and have been disabled in `plugin_config` + // Filter out components that have slots and have been disabled in `plugin_config` .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) .filter((o) => o.module.slots[slot]) .map((o) => o.module.slots[slot]) - ); + ) + .filter((component) => !component.isExcluded || !component.isExcluded({...props, config: pluginConfig})); } -export function isSlotEmpty(slot) { - return getSlotComponents(slot).length === 0; +export function isSlotEmpty(slot, pluginConfig, props) { + return getSlotComponents(slot, pluginConfig, props).length === 0; } /** * Returns React Elements for given slot. */ -export function getSlotElements(slot, props = {}) { - return getSlotComponents(slot) - .map((component, i) => React.createElement(component, {key: i, ...props})); +export function getSlotElements(slot, pluginConfig, props = {}) { + return getSlotComponents(slot, pluginConfig, props) + .map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig})); } function getComponentFragments(components) { diff --git a/client/coral-framework/hocs/excludeIf.js b/client/coral-framework/hocs/excludeIf.js new file mode 100644 index 000000000..ec2f0f27a --- /dev/null +++ b/client/coral-framework/hocs/excludeIf.js @@ -0,0 +1,4 @@ +export default (condition) => (BaseComponent) => { + BaseComponent.isExcluded = condition; + return BaseComponent; +}; diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index d62d62ae3..62e96444c 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,14 +1,5 @@ -import React from 'react'; -import {getDisplayName} from '../helpers/hoc'; - // TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38. -export default (fragments) => (WrappedComponent) => { - class WithFragments extends React.Component { - render() { - return ; - } - } - WithFragments.fragments = fragments; - WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`; - return WithFragments; +export default (fragments) => (BaseComponent) => { + BaseComponent.fragments = fragments; + return BaseComponent; }; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 92d676abe..5481b7a5e 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -7,6 +7,7 @@ const initialState = Map({ loggedIn: false, user: null, showSignInDialog: false, + signInDialogFocus: false, showCreateUsernameDialog: false, checkedInitialLogin: false, view: 'SIGNIN', @@ -29,13 +30,21 @@ const purge = (user) => { export default function auth (state = initialState, action) { switch (action.type) { + case actions.FOCUS_SIGNIN_DIALOG: + return state + .set('signInDialogFocus', true); + case actions.BLUR_SIGNIN_DIALOG: + return state + .set('signInDialogFocus', false); case actions.SHOW_SIGNIN_DIALOG : return state - .set('showSignInDialog', true); + .set('showSignInDialog', true) + .set('signInDialogFocus', true); case actions.HIDE_SIGNIN_DIALOG : return state.merge(Map({ isLoading: false, showSignInDialog: false, + signInDialogFocus: false, view: 'SIGNIN', error: '', passwordRequestFailure: null, diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 7f3476cfd..ed8b5cec4 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -1,2 +1,3 @@ export {default as withReaction} from './withReaction'; export {default as withFragments} from 'coral-framework/hocs/withFragments'; +export {default as excludeIf} from 'coral-framework/hocs/excludeIf'; diff --git a/plugin-api/beta/client/selectors/index.js b/plugin-api/beta/client/selectors/index.js new file mode 100644 index 000000000..09e25d453 --- /dev/null +++ b/plugin-api/beta/client/selectors/index.js @@ -0,0 +1 @@ +export const pluginConfigSelector = (state) => state.config.pluginConfig; diff --git a/plugins/talk-plugin-featured/client/containers/Tab.js b/plugins/talk-plugin-featured/client/containers/Tab.js index 0bcb49e13..f5d5678e6 100644 --- a/plugins/talk-plugin-featured/client/containers/Tab.js +++ b/plugins/talk-plugin-featured/client/containers/Tab.js @@ -1,5 +1,6 @@ import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; +import excludeIf from 'coral-framework/hocs/excludeIf'; import Tab from '../components/Tab'; // TODO: This is just example code, and needs to replaced by an actual implementation. @@ -12,6 +13,7 @@ const enhance = compose( } }`, }), + excludeIf((props) => props.asset.recentComments.length === 0) ); export default enhance(Tab); From d2e6a98d46a4d7b7bfd0b6b2f2b892e8aa469580 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 17 Jul 2017 18:06:51 +0700 Subject: [PATCH 33/47] connect aware "excludeIf" using our own hoc --- .../src/components/Stream.js | 6 ++--- .../src/containers/Stream.js | 2 ++ .../components/IfSlotIsEmpty.js | 9 +++++--- .../components/IfSlotIsNotEmpty.js | 9 +++++--- client/coral-framework/components/Slot.js | 10 +++++--- client/coral-framework/helpers/plugins.js | 23 ++++++++++++++----- client/coral-framework/hocs/connect.js | 6 +++++ .../client/containers/Tab.js | 8 ++++++- 8 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 client/coral-framework/hocs/connect.js diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index a547f10e8..5fde7c1df 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -75,7 +75,7 @@ class Stream extends React.Component { viewAllComments, auth: {loggedIn, user}, removeTag, - pluginConfig, + reduxState, editName } = this.props; const {keepCommentBox} = this.state; @@ -209,7 +209,7 @@ class Stream extends React.Component {
    - {getSlotComponents('streamTabs', pluginConfig, streamTabProps).map((PluginComponent) => ( + {getSlotComponents('streamTabs', reduxState, streamTabProps).map((PluginComponent) => ( - {getSlotComponents('streamTabPanes', pluginConfig, streamTabProps).map((PluginComponent) => ( + {getSlotComponents('streamTabPanes', reduxState, streamTabProps).map((PluginComponent) => ( ({ previousStreamTab: state.stream.previousTab, commentClassNames: state.stream.commentClassNames, pluginConfig: state.config.plugin_config, + reduxState: omit(state, 'apollo'), }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index b4dacf1eb..424a59bc7 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -2,11 +2,12 @@ import React from 'react'; import {connect} from 'react-redux'; import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; +import omit from 'lodash/omit'; -function IfSlotIsEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) { +function IfSlotIsEmpty({slot, className, reduxState, component: Component = 'div', children, ...rest}) { return ( - {isSlotEmpty(slot, pluginConfig, rest) ? children : null} + {isSlotEmpty(slot, reduxState, rest) ? children : null} ); } @@ -16,7 +17,9 @@ IfSlotIsEmpty.propTypes = { className: PropTypes.string, }; -const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); +const mapStateToProps = (state) => ({ + reduxState: omit(state, 'apollo'), +}); export default connect(mapStateToProps, null)(IfSlotIsEmpty); diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 820a528fe..3a41fffbd 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -2,11 +2,12 @@ import React from 'react'; import {connect} from 'react-redux'; import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; +import omit from 'lodash/omit'; -function IfSlotIsNotEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) { +function IfSlotIsNotEmpty({slot, className, reduxState, component: Component = 'div', children, ...rest}) { return ( - {!isSlotEmpty(slot, pluginConfig, rest) ? children : null} + {!isSlotEmpty(slot, reduxState, rest) ? children : null} ); } @@ -16,7 +17,9 @@ IfSlotIsNotEmpty.propTypes = { className: PropTypes.string, }; -const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); +const mapStateToProps = (state) => ({ + reduxState: omit(state, 'apollo'), +}); export default connect(mapStateToProps, null)(IfSlotIsNotEmpty); diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 2861a451c..b1df6647a 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -3,9 +3,11 @@ import cn from 'classnames'; import styles from './Slot.css'; import {connect} from 'react-redux'; import {getSlotElements} from 'coral-framework/helpers/plugins'; +import omit from 'lodash/omit'; -function Slot ({fill, inline = false, className, pluginConfig = {}, defaultComponent: DefaultComponent, ...rest}) { - let children = getSlotElements(fill, pluginConfig, rest); +function Slot ({fill, inline = false, className, reduxState, defaultComponent: DefaultComponent, ...rest}) { + let children = getSlotElements(fill, reduxState, rest); + const pluginConfig = reduxState.config.pluginConfig || {}; if (children.length === 0 && DefaultComponent) { children = ; } @@ -21,7 +23,9 @@ Slot.propTypes = { fill: React.PropTypes.string }; -const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config}); +const mapStateToProps = (state) => ({ + reduxState: omit(state, 'apollo'), +}); export default connect(mapStateToProps, null)(Slot); diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 55fdaf6c5..2ec377050 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -10,7 +10,8 @@ import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers} from 'coral-framework/services/store'; import camelize from './camelize'; -export function getSlotComponents(slot, pluginConfig, props = {}) { +export function getSlotComponents(slot, reduxState, props = {}) { + const pluginConfig = reduxState.config.pluginConfig || {}; return flatten(plugins // Filter out components that have slots and have been disabled in `plugin_config` @@ -19,18 +20,28 @@ export function getSlotComponents(slot, pluginConfig, props = {}) { .filter((o) => o.module.slots[slot]) .map((o) => o.module.slots[slot]) ) - .filter((component) => !component.isExcluded || !component.isExcluded({...props, config: pluginConfig})); + .filter((component) => { + if(!component.isExcluded) { + return true; + } + let resolvedProps = {...props, config: pluginConfig}; + if (component.mapStateToProps) { + resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; + } + return !component.isExcluded(resolvedProps); + }); } -export function isSlotEmpty(slot, pluginConfig, props) { - return getSlotComponents(slot, pluginConfig, props).length === 0; +export function isSlotEmpty(slot, reduxState, props) { + return getSlotComponents(slot, reduxState, props).length === 0; } /** * Returns React Elements for given slot. */ -export function getSlotElements(slot, pluginConfig, props = {}) { - return getSlotComponents(slot, pluginConfig, props) +export function getSlotElements(slot, reduxState, props = {}) { + const pluginConfig = reduxState.config.pluginConfig || {}; + return getSlotComponents(slot, reduxState, props) .map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig})); } diff --git a/client/coral-framework/hocs/connect.js b/client/coral-framework/hocs/connect.js new file mode 100644 index 000000000..c49fe8380 --- /dev/null +++ b/client/coral-framework/hocs/connect.js @@ -0,0 +1,6 @@ +import {connect} from 'react-redux'; + +export default (mapStateToProps, ...rest) => (BaseComponent) => { + BaseComponent.mapStateToProps = mapStateToProps; + return connect(mapStateToProps, ...rest)(BaseComponent); +}; diff --git a/plugins/talk-plugin-featured/client/containers/Tab.js b/plugins/talk-plugin-featured/client/containers/Tab.js index f5d5678e6..2ca0ac4a2 100644 --- a/plugins/talk-plugin-featured/client/containers/Tab.js +++ b/plugins/talk-plugin-featured/client/containers/Tab.js @@ -1,10 +1,14 @@ import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; import excludeIf from 'coral-framework/hocs/excludeIf'; +import connect from 'coral-framework/hocs/connect'; import Tab from '../components/Tab'; // TODO: This is just example code, and needs to replaced by an actual implementation. const enhance = compose( + connect((state) => ({ + stream: state.stream, + })), withFragments({ asset: gql` fragment TalkFeatured_Tab_asset on Asset { @@ -13,7 +17,9 @@ const enhance = compose( } }`, }), - excludeIf((props) => props.asset.recentComments.length === 0) + excludeIf((props) => { + return props.asset.recentComments.length === 0; + }) ); export default enhance(Tab); From 0e9d9067f9f4acc84d09f19c8d4f423d93eea0ad Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 17 Jul 2017 19:28:02 +0700 Subject: [PATCH 34/47] Use plugin-api and port plugins to it --- plugin-api/beta/client/hocs/index.js | 1 + plugin-api/beta/client/hocs/withReaction.js | 2 +- .../coral-plugin-offtopic/client/containers/OffTopicCheckbox.js | 2 +- .../coral-plugin-offtopic/client/containers/OffTopicFilter.js | 2 +- .../client/containers/ViewingOptions.js | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index ed8b5cec4..78408f6c2 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -1,3 +1,4 @@ export {default as withReaction} from './withReaction'; export {default as withFragments} from 'coral-framework/hocs/withFragments'; export {default as excludeIf} from 'coral-framework/hocs/excludeIf'; +export {default as connect} from 'coral-framework/hocs/connect'; diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 72de7f5c4..f7be6e701 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -1,7 +1,7 @@ import React from 'react'; import get from 'lodash/get'; import uuid from 'uuid/v4'; -import {connect} from 'react-redux'; +import {connect} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import {getDisplayName} from 'coral-framework/helpers/hoc'; import {compose, gql} from 'react-apollo'; diff --git a/plugins/coral-plugin-offtopic/client/containers/OffTopicCheckbox.js b/plugins/coral-plugin-offtopic/client/containers/OffTopicCheckbox.js index 286fa8d9d..f8471344b 100644 --- a/plugins/coral-plugin-offtopic/client/containers/OffTopicCheckbox.js +++ b/plugins/coral-plugin-offtopic/client/containers/OffTopicCheckbox.js @@ -1,7 +1,7 @@ -import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {addTag, removeTag} from 'plugin-api/alpha/client/actions'; import {commentBoxTagsSelector} from 'plugin-api/alpha/client/selectors'; +import {connect} from 'plugin-api/beta/client/hocs'; import OffTopicCheckbox from '../components/OffTopicCheckbox'; const mapStateToProps = (state) => ({ diff --git a/plugins/coral-plugin-offtopic/client/containers/OffTopicFilter.js b/plugins/coral-plugin-offtopic/client/containers/OffTopicFilter.js index 98f5f7aea..6bca55638 100644 --- a/plugins/coral-plugin-offtopic/client/containers/OffTopicFilter.js +++ b/plugins/coral-plugin-offtopic/client/containers/OffTopicFilter.js @@ -1,4 +1,4 @@ -import {connect} from 'react-redux'; +import {connect} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import {toggleCheckbox} from '../actions'; import {commentClassNamesSelector} from 'plugin-api/alpha/client/selectors'; diff --git a/plugins/coral-plugin-viewing-options/client/containers/ViewingOptions.js b/plugins/coral-plugin-viewing-options/client/containers/ViewingOptions.js index 767a9197d..cbe1e7fa3 100644 --- a/plugins/coral-plugin-viewing-options/client/containers/ViewingOptions.js +++ b/plugins/coral-plugin-viewing-options/client/containers/ViewingOptions.js @@ -1,4 +1,4 @@ -import {connect} from 'react-redux'; +import {connect} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import ViewingOptions from '../components/ViewingOptions'; import {openViewingOptions, closeViewingOptions} from '../actions'; From 41f5297d7815ba1ac6ab0cd58f8bd22c5f566edb Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 17 Jul 2017 19:32:48 +0700 Subject: [PATCH 35/47] Deprecate `isSlotEmpty` in the plugins api --- plugin-api/beta/client/services/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin-api/beta/client/services/index.js b/plugin-api/beta/client/services/index.js index ddaa346ab..4e94a782a 100644 --- a/plugin-api/beta/client/services/index.js +++ b/plugin-api/beta/client/services/index.js @@ -1,3 +1,9 @@ export {t} from 'coral-framework/services/i18n'; export {can} from 'coral-framework/services/perms'; -export {isSlotEmpty} from 'coral-framework/helpers/plugins'; +import {isSlotEmpty as ise} from 'coral-framework/helpers/plugins'; + +// @TODO: Deprecated. +export function isSlotEmpty(...args) { + console.warn('A plugin is using `isSlotEmpty` which has been deprecated, please port to the new API using the `IfSlotIsEmpty` and `IfSlotIsNotEmpty` components.'); + return ise(...args); +} From d90f87428ca15da2675f5882f4bb14000cb9bab5 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 17 Jul 2017 19:37:01 +0700 Subject: [PATCH 36/47] Remove changes in featured --- plugins/talk-plugin-featured/client/containers/Tab.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/talk-plugin-featured/client/containers/Tab.js b/plugins/talk-plugin-featured/client/containers/Tab.js index 2ca0ac4a2..0bcb49e13 100644 --- a/plugins/talk-plugin-featured/client/containers/Tab.js +++ b/plugins/talk-plugin-featured/client/containers/Tab.js @@ -1,14 +1,9 @@ import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; -import excludeIf from 'coral-framework/hocs/excludeIf'; -import connect from 'coral-framework/hocs/connect'; import Tab from '../components/Tab'; // TODO: This is just example code, and needs to replaced by an actual implementation. const enhance = compose( - connect((state) => ({ - stream: state.stream, - })), withFragments({ asset: gql` fragment TalkFeatured_Tab_asset on Asset { @@ -17,9 +12,6 @@ const enhance = compose( } }`, }), - excludeIf((props) => { - return props.asset.recentComments.length === 0; - }) ); export default enhance(Tab); From b091cf5221435efe147b75219aa143dadc72d489 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 17 Jul 2017 10:43:28 -0600 Subject: [PATCH 37/47] Added new TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS option --- README.md | 3 ++- config.js | 10 +++++++++- graph/mutators/comment.js | 9 ++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a629065ff..86e8000d0 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,12 @@ sign and verify tokens via a `HS256` algorithm. - `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using. - `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`. - `TALK_SMTP_PORT` (*required for email*) - SMTP port. -- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint. +- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - When `TRUE`, disables the dynamic setup endpoint. (Default `FALSE`) - `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout. - `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout. - `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment - `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`) +- `TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS` (_optional_) When `TRUE`, disables flagging of comments that match the suspect word filter. (Default `FALSE`) Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for alternative methods of loading configuration during development. diff --git a/config.js b/config.js index ce8931235..e3dd4578c 100644 --- a/config.js +++ b/config.js @@ -77,7 +77,15 @@ const CONFIG = { SMTP_HOST: process.env.TALK_SMTP_HOST, SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD, SMTP_PORT: process.env.TALK_SMTP_PORT, - SMTP_USERNAME: process.env.TALK_SMTP_USERNAME + SMTP_USERNAME: process.env.TALK_SMTP_USERNAME, + + //------------------------------------------------------------------------------ + // Flagging Config + //------------------------------------------------------------------------------ + + // DISABLE_AUTOFLAG_SUSPECT_WORDS is true when the suspect words that are + // matched should not be flagged. + DISABLE_AUTOFLAG_SUSPECT_WORDS: process.env.TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS === 'TRUE' }; //============================================================================== diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index f32172a4d..539bd674e 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -15,6 +15,10 @@ const { EDIT_COMMENT } = require('../../perms/constants'); +const { + DISABLE_AUTOFLAG_SUSPECT_WORDS +} = require('../../config'); + const debug = require('debug')('talk:graph:mutators:tags'); const plugins = require('../../services/plugins'); @@ -297,7 +301,10 @@ const createPublicComment = async (context, commentInput) => { // Otherwise just return the new comment. // TODO: Check why the wordlist is undefined - if (wordlist != null && wordlist.suspect != null) { + + // If the wordlist has matched the suspect word filter and we haven't disabled + // auto-flagging suspect words, then we should flag the comment! + if (wordlist != null && wordlist.suspect != null && !DISABLE_AUTOFLAG_SUSPECT_WORDS) { // TODO: this is kind of fragile, we should refactor this to resolve // all these const's that we're using like 'COMMENTS', 'FLAG' to be From d82735b2b5b308773ffce3e9fc9644fd8afa01fd Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 17 Jul 2017 10:50:40 -0600 Subject: [PATCH 38/47] Adjusted docs --- docs/install-microservices.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/install-microservices.md b/docs/install-microservices.md index eb3111c2d..668603920 100644 --- a/docs/install-microservices.md +++ b/docs/install-microservices.md @@ -15,8 +15,7 @@ To accomplish this, Talk has the ability to run with subsets of its overall func The Talk server serves several logically/architecturally distinct functions: * A web server that - * serves "public" assets (aka, the comment embed) - * "protected" assets (aka, the admin console) over http(s) + * serves "public" assets (aka, the comment embed) * the GraphQL endpoints * A web socket server that handles subscriptions. * A jobs processor that handles queued operations. @@ -50,9 +49,9 @@ With routing logic in front of the webserver cluster, separation between public Consider a microservice deployment if: -* you want to put access to admin routes behind a firewall * you are running plugins that require intensive job processing * you do not want to simplicity of single cluster horizontal scaling and want to tune the economy and performance of your install. +* You run into scaling issues serving websockets At scale, combining separate concerns in a single process makes it very difficult to understand what is taking up resources. With microservices, each server could be configured to sit behind it's own load balance and scale independently. Each variety of process can always have just enough resources. From d5dfd89d94a9aea2d65c1c975e9a781e9bfcf464 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 17 Jul 2017 10:54:54 -0600 Subject: [PATCH 39/47] review of docs --- docs/install-microservices.md | 56 ++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/docs/install-microservices.md b/docs/install-microservices.md index 668603920..061640dd9 100644 --- a/docs/install-microservices.md +++ b/docs/install-microservices.md @@ -6,9 +6,12 @@ permalink: install-microservices.html summary: --- -In Talk, we seek to deliver the simplicity of a monolith with the advantages of a microservice based infrastructure for those who want them. +In Talk, we seek to deliver the simplicity of a monolith with the advantages of +a microservice based infrastructure for those who want them. -To accomplish this, Talk has the ability to run with subsets of its overall functionality and contains architecture that allows them to operate logically as microservices when running in a single environment. +To accomplish this, Talk has the ability to run with subsets of its overall +functionality and contains architecture that allows them to operate logically as +microservices when running in a single environment. ## Talk Server Functionalities @@ -20,15 +23,21 @@ The Talk server serves several logically/architecturally distinct functions: * A web socket server that handles subscriptions. * A jobs processor that handles queued operations. -In the documentation so far, we've discussed how to deploy all of these functionalities bundled into a single monolith application. This is convenient as there is minimal configuration and horizontal scaling is as easy as upping the number of servers behind a single load balancer. +In the documentation so far, we've discussed how to deploy all of these +functionalities bundled into a single monolith application. This is convenient +as there is minimal configuration and horizontal scaling is as easy as upping +the number of servers behind a single load balancer. ## Separating Talk into Microservices -Talk can be run in two or more separate clusters of servers by enabling/disabling different bits of functionality: webserver, socket server and jobs server. +Talk can be run in two or more separate clusters of servers by +enabling/disabling different bits of functionality: webserver, socket server and +jobs server. Each microservice would deploy with the same codebase and configuration. -Note that the `cli serve` command, which is responsible for starting the server, contains flags that control whether `jobs` and `websockets` are enabled. +Note that the `cli serve` command, which is responsible for starting the server, +contains flags that control whether `jobs` and `websockets` are enabled. ``` talk :) ]$ bin/cli serve --help @@ -41,30 +50,49 @@ talk :) ]$ bin/cli serve --help ... ``` -Each Talk Microservice cluster can be deployed in an identical manner described in the other docs in this section with the omission of the `-j` and/or `-w` flags. +Each Talk Microservice cluster can be deployed in an identical manner described +in the other docs in this section with the omission of the `-j` and/or `-w` +flags. -With routing logic in front of the webserver cluster, separation between public and protected assets can be achieved. +With routing logic in front of the webserver cluster, separation between public +and protected assets can be achieved. ## When should I consider separating? Consider a microservice deployment if: * you are running plugins that require intensive job processing -* you do not want to simplicity of single cluster horizontal scaling and want to tune the economy and performance of your install. +* you do not want to simplicity of single cluster horizontal scaling and want to + tune the economy and performance of your install. * You run into scaling issues serving websockets -At scale, combining separate concerns in a single process makes it very difficult to understand what is taking up resources. With microservices, each server could be configured to sit behind it's own load balance and scale independently. Each variety of process can always have just enough resources. +At scale, combining separate concerns in a single process makes it very +difficult to understand what is taking up resources. With microservices, each +server could be configured to sit behind it's own load balance and scale +independently. Each variety of process can always have just enough resources. -An install that heavily utilizes the jobs queue could see delays in http service because of heavy jobs processes and/or delays in the execution of jobs processes due to increased server load. +An install that heavily utilizes the jobs queue could see delays in http service +because of heavy jobs processes and/or delays in the execution of jobs processes +due to increased server load as a result of Node's single thread infrustructure. ## Deployment Methodologies -Note that there is no flag to separate the http routes on the webserver. Separating the http server functionalities can be accomplished by the routing of various routes to the correct http server. This can ensure that sensitive areas, such as the `/admin/` route are not available outside the firewall. +Note that there is no flag to separate the http routes on the webserver. +Separating the http server functionalities can be accomplished by the routing of +various routes to the correct http server via an external upstream proxy like +Google Cloud's Load Balancer, AWS's ELB, or a websever like NGINX/Apache. This +can ensure that sensitive areas, such as the `/admin/` route are not available +outside the firewall. -Talk's Queue is backed by Redis, so as long as all Talk instances access the same Redis cluster no additional configuration is needed when launching an independent jobs cluster. +Talk's job processors are synchronized by Redis, so as long as all Talk +instances access the same Redis cluster no additional configuration is needed +when launching an independent jobs cluster. -If there are any features of Talk that you believe should be disable-able via server flags, please let us know and consider contributing it to the project! +If there are any features of Talk that you believe should be disable-able via +server flags, please let us know and consider contributing it to the project! ## Deployment Flows/Scripts -We do not currently support any microservice based deployment flows. If you develop one yourself that is completely based on open source tooling, please consider contributing it to the project! +We do not currently support any microservice based deployment flows. If you +develop one yourself that is completely based on open source tooling, please +consider contributing it to the project! From ab87500303fea51e62496307dec801f1cf517826 Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Mon, 17 Jul 2017 17:02:46 -0400 Subject: [PATCH 40/47] Fix email address env var in docs --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8323ca724..c19c5a38e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,7 +17,7 @@ sign and verify tokens via a `HS256` algorithm. - `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`) - `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`) - `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`) -- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the +- `TALK_SMTP_FROM_ADDRESS` (*required for email*) - the address to send emails from using the SMTP provider. - `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using. - `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using. From d99f6d89234cc1f350ce14986c55a4ae64305cd8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 20:44:22 +0700 Subject: [PATCH 41/47] Support lazily resolving slot fragments --- .../routes/Moderation/containers/Comment.js | 12 ++- .../Moderation/containers/UserDetail.js | 14 ++-- .../src/containers/Comment.js | 12 ++- .../src/containers/Stream.js | 13 ++- client/coral-framework/helpers/plugins.js | 64 +++----------- client/coral-framework/hocs/withQuery.js | 36 +++++++- .../services/graphqlRegistry.js | 84 +++++++++++++++---- client/coral-framework/utils/index.js | 5 ++ 8 files changed, 138 insertions(+), 102 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/containers/Comment.js b/client/coral-admin/src/routes/Moderation/containers/Comment.js index 8843188b8..d724682ab 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Comment.js +++ b/client/coral-admin/src/routes/Moderation/containers/Comment.js @@ -1,22 +1,21 @@ import {gql} from 'react-apollo'; import Comment from '../components/Comment'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; import withFragments from 'coral-framework/hocs/withFragments'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; -const pluginFragments = getSlotsFragments([ +const slots = [ 'adminCommentInfoBar', 'adminCommentContent', 'adminSideActions', 'adminCommentDetailArea', -]); +]; export default withFragments({ root: gql` fragment CoralAdmin_ModerationComment_root on RootQuery { __typename - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} } - ${pluginFragments.definitions('root')} `, comment: gql` fragment CoralAdmin_ModerationComment_comment on Comment { @@ -52,8 +51,7 @@ export default withFragments({ editing { edited } - ${pluginFragments.spreads('comment')} + ${getSlotFragmentSpreads(slots, 'comment')} } - ${pluginFragments.definitions('comment')} ` })(Comment); diff --git a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js index 0e36ed51a..6b476b861 100644 --- a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js @@ -4,8 +4,7 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import UserDetail from '../components/UserDetail'; import withQuery from 'coral-framework/hocs/withQuery'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; -import {getDefinitionName} from 'coral-framework/utils'; +import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import { changeUserDetailStatuses, clearUserDetailSelections, @@ -26,9 +25,9 @@ const commentConnectionFragment = gql` ${Comment.fragments.comment} `; -const pluginFragments = getSlotsFragments([ +const slots = [ 'userProfile', -]); +]; class UserDetailContainer extends React.Component { static propTypes = { @@ -80,7 +79,7 @@ export const withUserDetailQuery = withQuery(gql` id provider } - ${pluginFragments.spreads('user')} + ${getSlotFragmentSpreads(slots, 'user')} } totalComments: commentCount(query: {author_id: $author_id}) rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]}) @@ -90,11 +89,8 @@ export const withUserDetailQuery = withQuery(gql` }) { ...CoralAdmin_Moderation_CommentConnection } - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} } - ${Comment.fragments.comment} - ${pluginFragments.definitions('user')} - ${pluginFragments.definitions('root')} ${commentConnectionFragment} `, { options: ({id, moderation: {userDetailStatuses: statuses}}) => { diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 1ffac82cf..479c6f302 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -1,9 +1,9 @@ import {gql} from 'react-apollo'; import Comment from '../components/Comment'; import {withFragments} from 'coral-framework/hocs'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; -const pluginFragments = getSlotsFragments([ +const slots = [ 'streamQuestionArea', 'commentInputArea', 'commentInputDetailArea', @@ -12,15 +12,14 @@ const pluginFragments = getSlotsFragments([ 'commentContent', 'commentReactions', 'commentAvatar' -]); +]; export default withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { __typename - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} } - ${pluginFragments.definitions('root')} `, comment: gql` fragment CoralEmbedStream_Comment_comment on Comment { @@ -48,8 +47,7 @@ export default withFragments({ edited editableUntil } - ${pluginFragments.spreads('comment')} + ${getSlotFragmentSpreads(slots, 'comment')} } - ${pluginFragments.definitions('comment')} ` })(Comment); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 994d749f3..a6fb4656f 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -15,9 +15,8 @@ import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/strea import Stream from '../components/Stream'; import Comment from './Comment'; import {withFragments} from 'coral-framework/hocs'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; +import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {Spinner} from 'coral-ui'; -import {getDefinitionName} from 'coral-framework/utils'; import { findCommentInEmbedQuery, insertCommentIntoEmbedQuery, @@ -231,10 +230,10 @@ const LOAD_MORE_QUERY = gql` ${Comment.fragments.comment} `; -const pluginFragments = getSlotsFragments([ +const slots = [ 'streamTabs', 'streamTabPanes', -]); +]; const fragments = { root: gql` @@ -277,7 +276,7 @@ const fragments = { startCursor endCursor } - ${pluginFragments.spreads('asset')} + ${getSlotFragmentSpreads(slots, 'asset')} } me { status @@ -288,11 +287,9 @@ const fragments = { settings { organizationName } - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} ...${getDefinitionName(Comment.fragments.root)} } - ${pluginFragments.definitions('asset')} - ${pluginFragments.definitions('root')} ${Comment.fragments.root} ${commentFragment} `, diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 625a0ee3f..d60329baa 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -5,7 +5,6 @@ import merge from 'lodash/merge'; import plugins from 'pluginsConfig'; import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; -import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers, getStore} from 'coral-framework/services/store'; import camelize from './camelize'; @@ -35,62 +34,21 @@ export function getSlotElements(slot, props = {}) { .map((component, i) => React.createElement(component, {key: i, ...props})); } -function getComponentFragments(components) { - const res = components +export function getSlotFragments(slot, part) { + const components = uniq(flattenDeep(plugins + .filter((o) => o.module.slots ? o.module.slots[slot] : false) + .map((o) => o.module.slots[slot]) + )); + + const documents = components .map((c) => c.fragments) - .filter((fragments) => fragments) + .filter((fragments) => fragments && fragments[part]) .reduce((res, fragments) => { - Object.keys(fragments).forEach((key) => { - if (!(key in res)) { - res[key] = {spreads: [], definitions: []}; - } - res[key].spreads.push(getDefinitionName(fragments[key])); - res[key].definitions.push(fragments[key]); - }); + res.push(fragments[part]); return res; - }, {}); + }, []); - Object.keys(res).forEach((key) => { - - // Assemble arguments for `gql` to call it directly without using template literals. - res[key].spreads = `...${res[key].spreads.join('\n...')}\n`; - res[key].definitions = mergeDocuments(res[key].definitions); - }); - - return res; -} - -/** - * Returns an object that can be used to compose fragments or queries. - * - * Example: - * const pluginFragments = getSlotsFragments(['commentInfoBar', 'commentActions']); - * const rootFragment = gql` - * fragment Comment_root on RootQuery { - + ${pluginFragments.spreads('root')} - * } - * ${pluginFragments.definitions('root')} - * `; - */ -export function getSlotsFragments(slots) { - if (!Array.isArray(slots)) { - slots = [slots]; - } - const components = uniq(flattenDeep(slots.map((slot) => { - return plugins - .filter((o) => o.module.slots ? o.module.slots[slot] : false) - .map((o) => o.module.slots[slot]); - }))); - - const fragments = getComponentFragments(components); - return { - spreads(key) { - return (fragments[key] && fragments[key].spreads) || ''; - }, - definitions(key) { - return (fragments[key] && fragments[key].definitions) || ''; - }, - }; + return documents; } export function getGraphQLExtensions() { diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index c4e9bc375..ec190466c 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -15,14 +15,42 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => { * apply query options registered in the graphRegistry. */ export default (document, config = {}) => (WrappedComponent) => { - config = { + const wrappedConfig = { ...config, options: config.options || {}, - props: config.props || (({data}) => separateDataAndRoot(data)), + props: (args) => { + const wrappedArgs = { + ...args, + data: { + ...args.data, + subscribeToMore(stmArgs) { + + // Resolve document fragments before passing it to `apollo-client`. + return args.data.subscribeToMore({ + ...stmArgs, + document: resolveFragments(stmArgs.document), + }); + }, + fetchMore(lmArgs) { + + // Resolve document fragments before passing it to `apollo-client`. + return args.data.fetchMore({ + ...lmArgs, + query: resolveFragments(lmArgs.query), + }); + }, + }, + }; + return config.props + ? config.props(wrappedArgs) + : separateDataAndRoot(wrappedArgs.data); + }, }; const wrappedOptions = (data) => { - const base = (typeof config.options === 'function') ? config.options(data) : config.options; + const base = (typeof wrappedConfig.options === 'function') + ? wrappedConfig.options(data) + : wrappedConfig.options; const name = getDefinitionName(document); const configs = getQueryOptions(name); const reducerCallbacks = @@ -45,7 +73,7 @@ export default (document, config = {}) => (WrappedComponent) => { let memoized = null; const getWrapped = () => { if (!memoized) { - memoized = graphql(resolveFragments(document), {...config, options: wrappedOptions})(WrappedComponent); + memoized = graphql(resolveFragments(document), {...wrappedConfig, options: wrappedOptions})(WrappedComponent); } return memoized; }; diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 8b917c465..41d3e2d10 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -1,7 +1,8 @@ import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; -import {getGraphQLExtensions} from 'coral-framework/helpers/plugins'; +import {getGraphQLExtensions, getSlotFragments} from 'coral-framework/helpers/plugins'; import globalFragments from 'coral-framework/graphql/fragments'; import uniq from 'lodash/uniq'; +import {gql} from 'react-apollo'; const fragments = {}; const mutationOptions = {}; @@ -139,18 +140,47 @@ export function getQueryOptions(key) { } /** - * Get a document with a fragment named `key`, which contains - * all fragments added under this key. + * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. + * It parses the slot name and the resource and usees the plugin API to assemble + * the fragment document. */ -export function getFragmentDocument(key) { - init(); +function getSlotFragmentDocument(key) { + const match = key.match(/TalkSlot_(.*)_(.*)/); + if (!match) { + return ''; + } + const slot = match[1][0].toLowerCase() + match[1].substr(1); + const resource = match[2]; + const documents = getSlotFragments(slot, resource); + + if (documents.length === 0) { + return ''; + } + + const names = documents.map((d) => getDefinitionName(d)); + const typeName = getTypeName(documents[0]); + + // Assemble arguments for `gql` to call it directly without using template literals. + const main = ` + fragment ${key} on ${typeName} { + ...${names.join('\n...')}\n + } + `; + return mergeDocuments([main, ...documents]); +} + +/** + * getRegistryFragmentDocument assembles a fragment document using + * all registered fragment under given `key`. + */ +function getRegistryFragmentDocument(key) { if (!(key in fragments)) { return ''; } - let documents = fragments[key] ? fragments[key].documents : []; - let fields = fragments[key] ? `...${fragments[key].names.join('\n...')}\n` : ' __typename'; + let documents = fragments[key].documents; + let fields = `...${fragments[key].names.join('\n...')}\n`; // Assemble arguments for `gql` to call it directly without using template literals. const main = ` @@ -161,6 +191,16 @@ export function getFragmentDocument(key) { return mergeDocuments([main, ...documents]); } +/** + * getFragmentDocument returns a fragment that assembles all registered + * fragments under given `key` or if `key` refers to Slot fragments it will + * return the slot fragments specified by this key. + */ +export function getFragmentDocument(key) { + init(); + return getRegistryFragmentDocument(key) || getSlotFragmentDocument(key); +} + // The fragments and configs are lazily loaded to allow circular dependencies to work. // TODO: We might want to change this to an explicit add after we have lazy Queries and Mutations. let initialized = false; @@ -178,19 +218,35 @@ function init() { getGraphQLExtensions().forEach((ext) => add(ext)); } +/** + * resolveFragments finds fragment spread names and attachs + * the related fragment document to the given root document. + */ export function resolveFragments(document) { if (document.loc.source) { - // resolve fragments from registry - const matchedSubFragments = document.loc.source.body.match(/\.\.\.(.*)/g) || []; - const subFragments = - uniq(matchedSubFragments.map((f) => f.replace('...', ''))) - .map((key) => getFragmentDocument(key)) - .filter((i) => i); + const subFragments = []; + let body = document.loc.source.body; + + const matchedSubFragments = body.match(/\.\.\.(.*)/g) || []; + + uniq(matchedSubFragments.map((f) => f.replace('...', ''))) + .forEach((key) => { + const doc = getFragmentDocument(key); + if (doc) { + subFragments.push(doc); + } else if(key.startsWith('TalkSlot_')) { + + // Remove fragment spread of slots with zero fragments. + body = body.replace(`...${key}\n`, ''); + } + }); if (subFragments.length > 0) { - return mergeDocuments([document, ...subFragments]); + return mergeDocuments([body, ...subFragments]); } + + return gql`${body}`; } else { console.warn('Can only resolve fragments from documents definied using the gql tag.'); } diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 1866005be..0608179ed 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -1,5 +1,6 @@ import {gql} from 'react-apollo'; import t from 'coral-framework/services/i18n'; +import {capitalize} from 'coral-framework/helpers/strings'; export const getTotalActionCount = (type, comment) => { return comment.action_summaries @@ -144,3 +145,7 @@ export function forEachError(error, callback) { callback({error: e, msg}); }); } + +export function getSlotFragmentSpreads(slots, part) { + return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${part}`).join('\n...')}\n`; +} From 5fee26da0e35714df89399aef55ab226963e44de Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 20:50:00 +0700 Subject: [PATCH 42/47] Document getSlotFragmentSpreads --- client/coral-framework/utils/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 0608179ed..73a6a6676 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -146,6 +146,12 @@ export function forEachError(error, callback) { }); } -export function getSlotFragmentSpreads(slots, part) { - return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${part}`).join('\n...')}\n`; +/** + * getSlotFragmentSpreads will return a string in the + * expected format for slot fragments, given `slots` and `resource`. + * e.g. `getSlotsFragmentSpreads(['slotName'], 'root')` returns + * `...TalkSlot_SlotName_root`. + */ +export function getSlotFragmentSpreads(slots, resource) { + return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${resource}`).join('\n...')}\n`; } From 52cd7bcce4ca2af214e576f2102c06c6f572661c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 20:51:21 +0700 Subject: [PATCH 43/47] Expose getSlotFragmentSpread in plugin-api --- client/coral-framework/utils/index.js | 2 +- plugin-api/beta/client/utils/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 plugin-api/beta/client/utils/index.js diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 73a6a6676..61413e03d 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -149,7 +149,7 @@ export function forEachError(error, callback) { /** * getSlotFragmentSpreads will return a string in the * expected format for slot fragments, given `slots` and `resource`. - * e.g. `getSlotsFragmentSpreads(['slotName'], 'root')` returns + * e.g. `getSlotFragmentSpreads(['slotName'], 'root')` returns * `...TalkSlot_SlotName_root`. */ export function getSlotFragmentSpreads(slots, resource) { diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js new file mode 100644 index 000000000..44c3af6c0 --- /dev/null +++ b/plugin-api/beta/client/utils/index.js @@ -0,0 +1 @@ +export {getSlotFragmentSpreads} from 'coral-framework/utils'; From 55ff0bcf8c767a665704e84395e88f5eb22f7200 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 22:57:54 +0700 Subject: [PATCH 44/47] Recursively resolve fragments --- .../services/graphqlRegistry.js | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 41d3e2d10..49d06c5c6 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -225,27 +225,48 @@ function init() { export function resolveFragments(document) { if (document.loc.source) { + // Remember keys that we have already resolved. + const resolvedKeys = []; + + // Spreads from slots that are empty and need to be removed. + // (works around the issue that we don't know the resource type + // if we don't have a fragment) + const spreadsToBeRemoved = []; + + // fragments to be attached. const subFragments = []; + + // body contains the final result. let body = document.loc.source.body; - const matchedSubFragments = body.match(/\.\.\.(.*)/g) || []; + let done = false; + while (!done) { + done = true; - uniq(matchedSubFragments.map((f) => f.replace('...', ''))) - .forEach((key) => { - const doc = getFragmentDocument(key); - if (doc) { - subFragments.push(doc); - } else if(key.startsWith('TalkSlot_')) { + const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || []; + uniq(matchedSubFragments.map((f) => f.replace('...', ''))) + .filter((key) => resolvedKeys.indexOf(key) === -1) + .forEach((key) => { + const doc = getFragmentDocument(key); + if (doc) { + subFragments.push(doc); - // Remove fragment spread of slots with zero fragments. - body = body.replace(`...${key}\n`, ''); - } - }); + // We found a new fragment, so we are not done yet. + done = false; + } else if(key.startsWith('TalkSlot_')) { + spreadsToBeRemoved.push(key); + } + resolvedKeys.push(key); + }); - if (subFragments.length > 0) { - return mergeDocuments([body, ...subFragments]); + body = mergeDocuments([body, ...subFragments]).loc.source.body; } + spreadsToBeRemoved.forEach((key) => { + const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g'); + body = body.replace(regex, ''); + }); + return gql`${body}`; } else { console.warn('Can only resolve fragments from documents definied using the gql tag.'); From 99b8b4e90bb45cf56357b188823700fdfd9121d0 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 19 Jul 2017 15:34:04 -0400 Subject: [PATCH 45/47] Bump version 2.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 76b0c299a..3e3254fd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "2.4.0", + "version": "2.5.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { From 5d8528fdb5c28e71ce6a1eb7b8b04ec93b9cf22e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 20 Jul 2017 03:16:05 +0700 Subject: [PATCH 46/47] Replace deprecated commentId --- .../client/components/PermalinkButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js index cca8718b5..e8df91fd9 100644 --- a/plugins/talk-plugin-permalink/client/components/PermalinkButton.js +++ b/plugins/talk-plugin-permalink/client/components/PermalinkButton.js @@ -62,7 +62,7 @@ export default class PermalinkButton extends React.Component { render () { const {copySuccessful, copyFailure, popoverOpen} = this.state; - const {asset} = this.props; + const {asset, comment} = this.props; return (
    @@ -83,7 +83,7 @@ export default class PermalinkButton extends React.Component { className={cn(styles.input, `${name}-copy-field`)} type='text' ref={(input) => this.permalinkInput = input} - defaultValue={`${asset.url}?commentId=${this.props.commentId}`} + defaultValue={`${asset.url}?commentId=${comment.id}`} />