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 000000000..2e828f0fb Binary files /dev/null and b/plugins/talk-plugin-avatar/client/assets/avatar-placeholder.png differ 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" +}