diff --git a/.babelrc b/.babelrc index 41d27bf34..6186cefaf 100644 --- a/.babelrc +++ b/.babelrc @@ -5,7 +5,6 @@ ], "plugins": [ "add-module-exports", - "transform-async-to-generator", "transform-class-properties", "transform-decorators-legacy", "transform-object-assign", diff --git a/.eslintrc.json b/.eslintrc.json index 34b46b83b..293657679 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,9 @@ "node": true }, "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2017 + }, "rules": { "indent": ["error", 2 diff --git a/Dockerfile b/Dockerfile index 7cd06e673..fc0b3aca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,5 @@ FROM node:7.6 -# Add node-gyp for bcrypt build support -RUN yarn global add node-gyp - # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app diff --git a/client/.babelrc b/client/.babelrc new file mode 100644 index 000000000..27289288c --- /dev/null +++ b/client/.babelrc @@ -0,0 +1,6 @@ +{ + "extends": "../.babelrc", + "plugins": [ + "transform-async-to-generator", + ] +} diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 4a2314926..6425507d0 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -14,28 +14,35 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
{lang.t('configure.dashboard')} {lang.t('configure.moderate')} - {lang.t('configure.streams')} - {lang.t('configure.community')} diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 09a94aa55..7bc1a21f4 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -1,3 +1,7 @@ .Reply { position: relative; } + +.Comment { + +} diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 8a81f9fe6..72449c906 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import FlagComment from 'coral-plugin-flags/FlagComment'; import LikeButton from 'coral-plugin-likes/LikeButton'; +import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton'; import LoadMore from 'coral-embed-stream/src/LoadMore'; import styles from './Comment.css'; @@ -25,6 +26,11 @@ const getActionSummary = (type, comment) => comment.action_summaries .filter((a) => a.__typename === type)[0]; const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ; +// hold actions links (e.g. Like, Reply) along the comment footer +const ActionButton = ({children}) => { + return { children }; +}; + class Comment extends React.Component { constructor(props) { @@ -74,7 +80,13 @@ class Comment extends React.Component { id: PropTypes.string.isRequired, name: PropTypes.string.isRequired }).isRequired - }).isRequired + }).isRequired, + + // dispatch action to add a tag to a comment + addCommentTag: React.PropTypes.func, + + // dispatch action to remove a tag from a comment + removeCommentTag: React.PropTypes.func, } render () { @@ -94,7 +106,9 @@ class Comment extends React.Component { loadMore, setActiveReplyBox, activeReplyBox, - deleteAction + deleteAction, + addCommentTag, + removeCommentTag, } = this.props; const like = getActionSummary('LikeActionSummary', comment); @@ -103,6 +117,27 @@ class Comment extends React.Component { let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`; commentClass += highlighted === comment.id ? ' highlighted-comment' : ''; + // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) + const notifyOnError = (fn, errorToMessage) => async () => { + if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;} + try { + return await fn(); + } catch (error) { + addNotification('error', errorToMessage(error)); + throw error; + } + }; + + const addBestTag = notifyOnError(() => addCommentTag({ + id: comment.id, + tag: BEST_TAG, + }), () => 'Failed to tag comment as best'); + + const removeBestTag = notifyOnError(() => removeCommentTag({ + id: comment.id, + tag: BEST_TAG, + }), () => 'Failed to remove best comment tag'); + return (
{ isStaff(comment.tags) - ? + ? Staff + : null } + + { commentIsBest(comment) + ? : null } -
- - setActiveReplyBox(comment.id)} - parentCommentId={parentId || comment.id} - currentUserId={currentUser && currentUser.id} - banned={false} /> +
+ + + + + setActiveReplyBox(comment.id)} + parentCommentId={parentId || comment.id} + currentUserId={currentUser && currentUser.id} + banned={false} /> + + + + + +
-
- - +
+ + + + + +
{ activeReplyBox === comment.id @@ -172,6 +227,8 @@ class Comment extends React.Component { postLike={postLike} postFlag={postFlag} deleteAction={deleteAction} + addCommentTag={addCommentTag} + removeCommentTag={removeCommentTag} showSignInDialog={showSignInDialog} reactKey={reply.id} key={reply.id} diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index da9fd4b6e..5fc1673a1 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -13,7 +13,7 @@ const {addNotification, clearNotification} = notificationActions; const {fetchAssetSuccess} = assetActions; import {queryStream} from 'coral-framework/graphql/queries'; -import {postComment, postFlag, postLike, postDontAgree, deleteAction} from 'coral-framework/graphql/mutations'; +import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations'; import {editName} from 'coral-framework/actions/user'; import {updateCountCache} from 'coral-framework/actions/asset'; import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework'; @@ -55,7 +55,13 @@ class Embed extends Component { data: React.PropTypes.shape({ loading: React.PropTypes.bool, error: React.PropTypes.object - }).isRequired + }).isRequired, + + // dispatch action to add a tag to a comment + addCommentTag: React.PropTypes.func, + + // dispatch action to remove a tag from a comment + removeCommentTag: React.PropTypes.func, } componentDidMount () { @@ -194,22 +200,26 @@ class Embed extends Component { assetId={asset.id} updateCountCache={this.props.updateCountCache} /> - +
+ +
+
{ comments.map(comment => ({ + addCommentTag: ({id, tag}) => { + return mutate({ + variables: { + id, + tag + } + }); + }}), +}); + +export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, { + props: ({mutate}) => ({ + removeCommentTag: ({id, tag}) => { + return mutate({ + variables: { + id, + tag + } + }); + }}), +}); diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql new file mode 100644 index 000000000..3826b0703 --- /dev/null +++ b/client/coral-framework/graphql/mutations/removeCommentTag.graphql @@ -0,0 +1,13 @@ +mutation RemoveCommentTag ($id: ID!, $tag: String!) { + removeCommentTag(id:$id, tag:$tag) { + comment { + id + tags { + name + } + } + errors { + translation_key + } + } +} diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js new file mode 100644 index 000000000..ac0e75aaf --- /dev/null +++ b/client/coral-plugin-best/BestButton.js @@ -0,0 +1,106 @@ +import React, {Component, PropTypes} from 'react'; +import {I18n} from '../coral-framework'; +import translations from './translations.json'; +import classnames from 'classnames'; + +// tag string for best comments +export const BEST_TAG = 'BEST'; +export const commentIsBest = ({tags} = {}) => { + const isBest = Array.isArray(tags) && tags.some(t => t.name === BEST_TAG); + return isBest; +}; + +const name = 'coral-plugin-best'; +const lang = new I18n(translations); + +// It would be best if the backend/api held this business logic +const canModifyBestTag = ({roles = []} = {}) => roles && ['ADMIN', 'MODERATOR'].some(role => roles.includes(role)); + +// Put this on a comment to show that it is best +export const BestIndicator = ({children = favorite}) => ( + + { children } + +); + +/** + * Component that only renders children if the provided user prop can modify best tags + */ +export const IfUserCanModifyBest = ({user, children}) => { + if ( ! ( user && canModifyBestTag(user))) {return null;} + return children; +}; + +/** + * Button that lets a moderator tag a comment as "Best". + * Used to recognize really good comments. + */ +export class BestButton extends Component { + static propTypes = { + + // whether the comment is already tagged as best + isBest: PropTypes.bool.isRequired, + + // set that this comment is best + addBest: PropTypes.func.isRequired, + + // remove the best status + removeBest: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.onClickAddBest = this.onClickAddBest.bind(this); + this.onClickRemoveBest = this.onClickRemoveBest.bind(this); + } + + state = { + isSaving: false + } + + async onClickAddBest(e) { + e.preventDefault(); + const {addBest} = this.props; + if ( ! addBest) { + console.warn('BestButton#onClickAddBest called even though there is no addBest prop. doing nothing'); + return; + } + this.setState({isSaving: true}); + try { + await addBest(); + } finally { + this.setState({isSaving: false}); + } + } + + async onClickRemoveBest(e) { + e.preventDefault(); + const {removeBest} = this.props; + if ( ! removeBest) { + console.warn('BestButton#onClickAddBest called even though there is no removeBest prop. doing nothing'); + return; + } + this.setState({isSaving: true}); + try { + await removeBest(); + } finally { + this.setState({isSaving: false}); + } + } + + render() { + const {isBest, addBest, removeBest} = this.props; + const {isSaving} = this.state; + const disabled = isSaving || ! (isBest ? removeBest : addBest); + return ( + + ); + } +} diff --git a/client/coral-plugin-best/translations.json b/client/coral-plugin-best/translations.json new file mode 100644 index 000000000..c7dd42be8 --- /dev/null +++ b/client/coral-plugin-best/translations.json @@ -0,0 +1,12 @@ +{ + "en": { + "setBest": "Tag as Best", + "unsetBest": "Untag as Best", + "commentIsBest": "This comment is one of the best" + }, + "es": { + "like": "Establecer como mejor", + "liked": "Desarmado como mejor", + "commentIsBest": "Este comentario es uno de los mejores" + } +} diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 9b3abfb00..51854c57f 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -57,7 +57,7 @@ class LikeButton extends Component { }; return
-