diff --git a/Dockerfile b/Dockerfile index 90c08057f..73885d628 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:7.9 +FROM node:7.8 # Create app directory RUN mkdir -p /usr/src/app diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 34c321448..06d006b42 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -3,12 +3,12 @@ FROM coralproject/talk:latest # Bundle app source ONBUILD COPY . /usr/src/app -# At this stage, we need to install the development dependancies again because +# At this stage, we need to install the development dependancies again because # we need to have webpack available. We then build the new dependancies and # clear out the development dependancies again. After this we of course need to # clear out the yarn cache, this saves quite a lot of size. ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \ NODE_ENV=production cli plugins reconcile && \ NODE_ENV=production yarn build && \ - NODE_ENV=production yarn install --production && \ + NODE_ENV=production yarn install --production --force && \ yarn cache clean \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index 18244f635..49033acc8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -174,8 +174,8 @@ and testing purposes. There are some runtime requirements for running Talk from source: -- [Node](https://nodejs.org/) v7.9 or later -- [Yarn](https://yarnpkg.com/) v0.22.0 or later +- [Node](https://nodejs.org/) ~7.8 +- [Yarn](https://yarnpkg.com/) ^0.22.0 _Please be sure to check the versions of these requirements. Incorrect versions of these may lead to unexpected errors!_ diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 61352b921..3487f9fde 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -116,7 +116,7 @@ class ModerationContainer extends Component { let asset; - if (data.loading) { + if (!('premodCount' in data)) { return
; } diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index a2bbbf092..b18f239e4 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -10,6 +10,7 @@ import FlagBox from './FlagBox'; import CommentType from './CommentType'; import ActionButton from 'coral-admin/src/components/ActionButton'; import BanUserButton from 'coral-admin/src/components/BanUserButton'; +import {getActionSummary} from 'coral-framework/utils'; const linkify = new Linkify(); @@ -17,25 +18,27 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-admin/src/translations.json'; const lang = new I18n(translations); -const Comment = ({actions = [], ...props}) => { - const links = linkify.getMatches(props.comment.body); +const Comment = ({actions = [], comment, ...props}) => { + const links = linkify.getMatches(comment.body); const linkText = links ? links.map(link => link.raw) : []; - const actionSummaries = props.comment.action_summaries; + const flagActionSummaries = getActionSummary('FlagActionSummary', comment); + const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction'); + return (
  • - {props.comment.user.name} + {comment.user.name} - {timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} + {timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - props.showBanUserDialog(props.comment.user, props.comment.id, props.comment.status !== 'REJECTED')} /> + props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
    - {props.comment.user.status === 'banned' ? + {comment.user.status === 'banned' ? {lang.t('comment.banned_user')} @@ -43,16 +46,16 @@ const Comment = ({actions = [], ...props}) => { : null}
    - Story: {props.comment.asset.title} + Story: {comment.asset.title} {!props.currentAsset && ( - Moderate → + Moderate → )}

    + textToHighlight={comment.body} />

    {links ? Contains Link : null} @@ -60,16 +63,20 @@ const Comment = ({actions = [], ...props}) => { {actions.map((action, i) => props.acceptComment({commentId: props.comment.id})} - rejectComment={() => props.rejectComment({commentId: props.comment.id})} + user={comment.user} + acceptComment={() => props.acceptComment({commentId: comment.id})} + rejectComment={() => props.rejectComment({commentId: comment.id})} /> )}
    - {actionSummaries && } + { + flagActions && flagActions.length + ? + : null + }
  • ); }; @@ -83,6 +90,7 @@ Comment.propTypes = { comment: PropTypes.shape({ body: PropTypes.string.isRequired, action_summaries: PropTypes.array, + actions: PropTypes.array, created_at: PropTypes.string.isRequired, user: PropTypes.shape({ status: PropTypes.string diff --git a/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.css b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.css index fa7bb0cca..5b8ea4f91 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.css +++ b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.css @@ -53,3 +53,17 @@ font-size: 12px; } } + +.lessDetail { + display: inline-block; + margin-right: 10px; +} + +.subDetail { + font-weight: normal; + color: #888; + + span { + color: black; + } +} diff --git a/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js index 00b7d4273..62b4e554d 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js @@ -1,6 +1,17 @@ import React, {Component, PropTypes} from 'react'; import {Icon} from 'coral-ui'; import styles from './FlagBox.css'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); + +const shortReasons = { + 'This comment is offensive': lang.t('modqueue.offensive'), + 'This looks like an ad/marketing': lang.t('modqueue.spam/ads'), + 'This user is impersonating': lang.t('modqueue.impersonating'), + 'I don\'t like this username': lang.t('modqueue.dont-like-username'), + 'Other': lang.t('modqueue.other') +}; class FlagBox extends Component { constructor () { @@ -16,27 +27,50 @@ class FlagBox extends Component { })); } + reasonMap = (reason) => { + const shortReason = shortReasons[reason]; + + // if the short reason isn't found, just return the long one. + return shortReason ? shortReason : reason; + } + render() { - const {props} = this; + const {actionSummaries, actions} = this.props; + const {showDetail} = this.state; + return (
    -

    Flags ({props.actionSummaries.length}):

    +

    Flags ({actionSummaries.length}):

      - {props.actionSummaries.map((action, i) => -
    • {!action.reason ? No reason provided : action.reason} ({action.count})
    • + {actionSummaries.map((action, i) => +
    • {this.reasonMap(action.reason)} ({action.count})
    • )}
    - {/* More detail*/} + {showDetail ? lang.t('modqueue.less-detail') : lang.t('modqueue.more-detail')}
    - {this.state.showDetail && (
    -
      - {props.actionSummaries.map((action, i) => -
    • {!action.reason ? No reason provided : action.reason} ({action.count})
    • - )} -
    -
    )} + {showDetail && ( +
    +
      + {actionSummaries.map((summary, i) => { + + const actionList = actions.filter(a => a.reason === summary.reason); + + return ( +
    • + {this.reasonMap(summary.reason)} ({summary.count}) +
        + { + actionList.map((action, j) =>
      • {action.user.username} {action.message}
      • ) + } +
      +
    • + ); + })} +
    +
    + )}
    ); @@ -44,7 +78,14 @@ class FlagBox extends Component { } FlagBox.propTypes = { - actionSummaries: PropTypes.array.isRequired + actionSummaries: PropTypes.arrayOf(PropTypes.shape({ + reason: PropTypes.string, + count: PropTypes.number + })).isRequired, + actions: PropTypes.arrayOf(PropTypes.shape({ + message: PropTypes.string, + user: PropTypes.shape({username: PropTypes.string}) + })).isRequired }; export default FlagBox; diff --git a/client/coral-admin/src/graphql/fragments/commentView.graphql b/client/coral-admin/src/graphql/fragments/commentView.graphql index e78c28a28..51b0a3f44 100644 --- a/client/coral-admin/src/graphql/fragments/commentView.graphql +++ b/client/coral-admin/src/graphql/fragments/commentView.graphql @@ -12,4 +12,19 @@ fragment commentView on Comment { id title } + action_summaries { + count + ... on FlagActionSummary { + reason + } + } + actions { + ... on FlagAction { + reason + message + user { + username + } + } + } } diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index f0e8e5b70..7d03b5092 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -55,7 +55,7 @@ export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => statuses, asset_id }, - updateQuery: (oldData, {fetchMoreResult:{data:{comments}}}) => { + updateQuery: (oldData, {fetchMoreResult:{comments}}) => { return { ...oldData, [tab]: [ diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql index b4f2a20a0..da8760be9 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -15,12 +15,6 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) { sort: $sort }) { ...commentView - action_summaries { - count - ... on FlagActionSummary { - reason - } - } } rejected: comments(query: { statuses: [REJECTED], diff --git a/client/coral-admin/src/services/client.js b/client/coral-admin/src/services/client.js index 7f6a0567d..7d65f3f92 100644 --- a/client/coral-admin/src/services/client.js +++ b/client/coral-admin/src/services/client.js @@ -1,7 +1,9 @@ import ApolloClient, {addTypename} from 'apollo-client'; import getNetworkInterface from './transport'; +import fragmentMatcher from './fragmentMatcher'; export const client = new ApolloClient({ + fragmentMatcher, addTypename: true, queryTransformer: addTypename, dataIdFromObject: (result) => { diff --git a/client/coral-admin/src/services/fragmentMatcher.js b/client/coral-admin/src/services/fragmentMatcher.js new file mode 100644 index 000000000..daa01a538 --- /dev/null +++ b/client/coral-admin/src/services/fragmentMatcher.js @@ -0,0 +1,33 @@ +import {IntrospectionFragmentMatcher} from 'apollo-client'; + +// TODO this is a short-term fix +// we need to set up something to query the server for the schema before ApolloClient initialization +// https://github.com/apollographql/apollo-client/issues/1555#issuecomment-295834774 +const fm = new IntrospectionFragmentMatcher({ + introspectionQueryResultData: { + __schema: { + types: [ + { + kind: 'INTERFACE', + name: 'Action', + possibleTypes: [ + {name: 'FlagAction'}, + {name: 'LikeAction'}, + {name: 'DontAgreeAction'} + ], + }, + { + kind: 'INTERFACE', + name: 'ActionSummary', + possibleTypes: [ + {name: 'FlagActionSummary'}, + {name: 'LikeActionSummary'}, + {name: 'DontAgreeActionSummary'} + ], + } + ], + }, + } +}); + +export default fm; diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 911411523..f317b71a7 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -51,7 +51,14 @@ "singleview": "Toggle single comment edit view", "thismenu": "Open this menu", "emptyqueue": "No more comments to moderate! You're all caught up. Go have some ☕️", - "showshortcuts": "Show Shortcuts" + "showshortcuts": "Show Shortcuts", + "more-detail": "More detail", + "less-detail": "Less detail", + "dont-like-username": "Don't like username", + "impersonating": "Impersonating", + "offensive": "Offensive", + "spam/ads": "Spam/Ads", + "other": "Other" }, "comment": { "flagged": "flagged", @@ -221,7 +228,14 @@ "shortcuts": "Atajos de teclado", "close": "Cerrar", "emptyqueue": "No se encontro ningún usuario. Están escondidos.", - "showshortcuts": "Mostrar atajos" + "showshortcuts": "Mostrar atajos", + "more-detail": "Mas detalle", + "less-detail": "Menos detalle", + "dont-like-username": "No me gusta ese nombre de usuario", + "impersonating": "Suplantación", + "offensive": "Ofensivo", + "spam/ads": "Spam/Propaganda", + "other": "Otros" }, "comment": { "flagged": "marcado", diff --git a/client/coral-embed-stream/src/AppRouter.js b/client/coral-embed-stream/src/AppRouter.js new file mode 100644 index 000000000..e2553889c --- /dev/null +++ b/client/coral-embed-stream/src/AppRouter.js @@ -0,0 +1,16 @@ +import React from 'react'; +import {Router, Route, browserHistory} from 'react-router'; + +import Embed from './containers/Embed'; +import SignInContainer from 'coral-sign-in/containers/SignInContainer'; + +const routes = ( +
    + + +
    +); + +const AppRouter = () => ; + +export default AppRouter; diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 7af19a816..c44e06282 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -18,15 +18,14 @@ 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 {Slot} from 'coral-framework'; +import Slot from 'coral-framework/components/Slot'; import LoadMore from './LoadMore'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import {TopRightMenu} from './TopRightMenu'; +import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils'; import styles from './Comment.css'; -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 @@ -124,9 +123,16 @@ class Comment extends React.Component { commentIsIgnored, } = this.props; - const like = getActionSummary('LikeActionSummary', comment); - const flag = getActionSummary('FlagActionSummary', comment); - const dontagree = getActionSummary('DontAgreeActionSummary', comment); + const likeSummary = getActionSummary('LikeActionSummary', comment); + const flagSummary = getActionSummary('FlagActionSummary', comment); + const dontAgreeSummary = getActionSummary('DontAgreeActionSummary', comment); + let myFlag = null; + if (iPerformedThisAction('FlagActionSummary', comment)) { + myFlag = flagSummary.find(s => s.current_user); + } else if (iPerformedThisAction('DontAgreeActionSummary', comment)) { + myFlag = dontAgreeSummary.find(s => s.current_user); + } + let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`; commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : ''; @@ -183,8 +189,10 @@ class Comment extends React.Component {
    + {/* TODO implmement iPerformedThisAction for the like */} u.id)} + ignoredUsers={this.props.data.myIgnoredUsers ? this.props.data.myIgnoredUsers.map(u => u.id) : []} auth={this.props.auth} comment={this.props.data.comment} commentCountCache={this.props.commentCountCache} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 456eb836a..5b9fbdaa7 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -1,7 +1,7 @@ import React, {PropTypes} from 'react'; +import {Button} from 'coral-ui'; import Comment from './Comment'; import CommentBox from 'coral-plugin-commentbox/CommentBox'; -import SignInContainer from 'coral-sign-in/containers/SignInContainer'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; @@ -16,8 +16,7 @@ class Stream extends React.Component { setActiveReplyBox = (reactKey) => { if (!this.props.auth.user) { - const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75; - this.props.showSignInDialog(offset); + this.props.showSignInDialog(); } else { this.props.setActiveReplyBox(reactKey); } @@ -40,7 +39,7 @@ class Stream extends React.Component { pluginProps, ignoreUser, ignoredUsers, - auth: {signInOffset, loggedIn, isAdmin, user}, + auth: {loggedIn, isAdmin, user}, comment, refetch, commentCountCache, @@ -104,11 +103,8 @@ class Stream extends React.Component {
    :

    {asset.settings.closedMessage}

    } - {!loggedIn && } - {loggedIn && user && } + {!loggedIn && } + {loggedIn && user && } {loggedIn && } {/* the highlightedComment is isolated after the user followed a permalink */} diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 937cec89f..429a4c790 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -17,7 +17,7 @@ import {setCommentCountCache, setActiveReplyBox, viewAllComments} from '../actio import {setActiveTab} from '../actions/embed'; import * as Stream from './Stream'; -const {logout, showSignInDialog, requestConfirmEmail} = authActions; +const {logout, showSignInDialog, requestConfirmEmail, checkLogin} = authActions; const {addNotification, clearNotification} = notificationActions; const {fetchAssetSuccess} = assetActions; @@ -25,6 +25,7 @@ class EmbedContainer extends React.Component { componentDidMount() { pym.sendMessage('childReady'); + this.props.checkLogin(); } componentWillUnmount() { @@ -135,7 +136,7 @@ const STREAM_QUERY = gql` __typename ...Stream_root } - ${Stream.fragment} + ${Stream.fragments.root} `; // get the counts of the top-level comments @@ -271,6 +272,7 @@ const mapDispatchToProps = dispatch => fetchAssetSuccess, addNotification, clearNotification, + checkLogin, editName, setCommentCountCache, viewAllComments, diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index ffdf3eac8..f3f6075b6 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -3,16 +3,18 @@ import {render} from 'react-dom'; import {ApolloProvider} from 'react-apollo'; import {client} from 'coral-framework/services/client'; -import {store, injectReducers} from 'coral-framework/services/store'; -import Embed from './containers/Embed'; import reducers from './reducers'; +import localStore, {injectReducers} from 'coral-framework/services/store'; +import AppRouter from './AppRouter'; injectReducers(reducers); +const store = (window.opener && window.opener.coralStore) ? window.opener.coralStore : localStore; + render( - + , document.querySelector('#coralStream') ); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 51028949b..13a200690 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -396,6 +396,7 @@ button.comment__action-button[disabled], margin-left: 20px; margin-top: 5px; width: 75%; + font-size: 16px; } /* Close comments */ @@ -475,3 +476,19 @@ button.comment__action-button[disabled], .coral-load-more-replies button.coral-load-more, .coral-new-comments button.coral-load-more{ width: initial; } + +@media (min-device-width : 300px) and (max-device-width : 420px) { + .commentActionsLeft.comment__action-container .coral-plugin-likes-button-text, + .commentActionsLeft.comment__action-container > div span { + display: none; + } + + .commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button { + visibility: collapse; + margin-left: -30px; + } + + .commentActionsLeft.comment__action-container .coral-plugin-replies-reply-button .coral-plugin-replies-icon { + visibility: visible; + } +} diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 11e7bd996..e112b2b6c 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -11,6 +11,16 @@ const ME_QUERY = gql` query Me { me { status + comments { + id + body + asset { + id + title + url + } + created_at + } } } `; @@ -22,8 +32,23 @@ function fetchMe() { } // Dialog Actions -export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset}); -export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG}); +export const showSignInDialog = () => dispatch => { + const signInPopUp = window.open( + '/embed/stream/login', + 'Login', + 'menubar=0,resizable=0,width=500,height=550,top=200,left=500' + ); + + signInPopUp.onbeforeunload = () => { + dispatch(checkLogin()); + fetchMe(); + }; + dispatch({type: actions.SHOW_SIGNIN_DIALOG}); +}; +export const hideSignInDialog = () => dispatch => { + dispatch({type: actions.HIDE_SIGNIN_DIALOG}); + window.close(); +}; export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST}); export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG}); @@ -47,29 +72,39 @@ export const createUsername = (userId, formData) => dispatch => { }); }; -export const changeView = view => dispatch => +export const changeView = view => dispatch => { dispatch({ type: actions.CHANGE_VIEW, view }); + switch(view) { + case 'SIGNUP': + window.resizeTo(500, 800); + break; + case 'FORGOT': + window.resizeTo(500, 400); + break; + default: + window.resizeTo(500, 550); + } +}; + export const cleanState = () => ({type: actions.CLEAN_STATE}); // Sign In Actions const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); -const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); + +// TODO: revisit login redux flow. +// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); +// const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInRequest()); return coralApi('/auth/local', {method: 'POST', body: formData}) - .then(({user}) => { - const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(signInSuccess(user, isAdmin)); - dispatch(hideSignInDialog()); - fetchMe(); - }) + .then(() => dispatch(hideSignInDialog())) .catch(error => { if (error.metadata) { @@ -121,7 +156,7 @@ export const facebookCallback = (err, data) => dispatch => { dispatch(signInFacebookSuccess(user)); dispatch(hideSignInDialog()); dispatch(showCreateUsernameDialog()); - fetchMe(); + dispatch(hideSignInDialog()); } catch (err) { dispatch(signInFacebookFailure(err)); return; diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js index 96750bffd..b85b69bcd 100644 --- a/client/coral-framework/index.js +++ b/client/coral-framework/index.js @@ -1,16 +1,12 @@ -import store from './services/store'; import pym from './services/PymConnection'; import I18n from './modules/i18n/i18n'; import actions from './actions'; -import Slot from './components/Slot'; // TODO (bc): Deprecate old actions. Spreading actions is now needed. export default { pym, - Slot, I18n, - store, actions, ...actions }; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index f9712c937..6e678cab9 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -28,8 +28,7 @@ export default function auth (state = initialState, action) { switch (action.type) { case actions.SHOW_SIGNIN_DIALOG : return state - .set('showSignInDialog', true) - .set('signInOffset', action.offset); + .set('showSignInDialog', true); case actions.HIDE_SIGNIN_DIALOG : return state.merge(Map({ isLoading: false, diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index a2a8251b3..f6ace1fd3 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -41,3 +41,5 @@ export function injectReducers(reducers) { storeReducers = {...storeReducers, ...reducers}; store.replaceReducer(combineReducers(storeReducers)); } + +window.coralStore = store; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 6d589cf23..4e9c29db7 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -1,7 +1,31 @@ -/** -* getActionSummary -* retrieves the action summary based on the type and the comment -*/ +export const getTotalActionCount = (type, comment) => { + return comment.action_summaries + .filter(s => s.__typename === type) + .reduce((total, summary) => { + return total + summary.count; + }, 0); +}; -export const getActionSummary = (type, comment) => - comment.action_summaries.filter(a => a.__typename === type)[0]; +export const iPerformedThisAction = (type, comment) => { + + // if there is a current_user on any of the ActionSummary(s), the user performed this action + return comment.action_summaries + .filter(a => a.__typename === type) + .some(a => a.current_user); +}; + +export const getMyActionSummary = (type, comment) => { + return comment.action_summaries + .filter(a => a.__typename === type) + .find(a => a.current_user); +}; + + /** + * getActionSummary + * retrieves the action summaries based on the type and the comment + * array could be length > 1, as in the case of FlagActionSummary + */ + +export const getActionSummary = (type, comment) => { + return comment.action_summaries.filter(a => a.__typename === type); +}; diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 0af98d913..3db1c6ad0 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -2,7 +2,7 @@ import React, {Component, PropTypes} from 'react'; import {I18n} from '../coral-framework'; import translations from './translations.json'; import {Button} from 'coral-ui'; -import {Slot} from 'coral-framework'; +import Slot from 'coral-framework/components/Slot'; import {connect} from 'react-redux'; const name = 'coral-plugin-commentbox'; diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 45110f407..16e616379 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -21,15 +21,14 @@ class FlagButton extends Component { // When the "report" button is clicked expand the menu onReportClick = () => { - const {currentUser, flag, deleteAction} = this.props; + const {currentUser, deleteAction, flaggedByCurrentUser, flag} = this.props; const {localPost, localDelete} = this.state; - const flagged = (flag && flag.current_user && !localDelete) || localPost; + const localFlagged = (flaggedByCurrentUser && !localDelete) || localPost; if (!currentUser) { - const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75; - this.props.showSignInDialog(offset); + this.props.showSignInDialog(); return; } - if (flagged) { + if (localFlagged) { this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true}); deleteAction(localPost || flag.current_user.id); } else if (this.state.showMenu){ @@ -130,9 +129,9 @@ class FlagButton extends Component { } render () { - const {flag, getPopupMenu} = this.props; + const {getPopupMenu, flaggedByCurrentUser} = this.props; const {localPost, localDelete} = this.state; - const flagged = (flag && flag.current_user && !localDelete) || localPost; + const flagged = (flaggedByCurrentUser && !localDelete) || localPost; const popupMenu = getPopupMenu[this.state.step](this.state.itemType); return
    diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 51854c57f..28382dbc2 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -27,16 +27,15 @@ class LikeButton extends Component { render() { const {like, id, postLike, deleteAction, showSignInDialog, currentUser} = this.props; + let {totalLikes: count} = this.props; const {localPost, localDelete} = this.state; const liked = (like && like.current_user && !localDelete) || localPost; - let count = like ? like.count : 0; if (localPost) {count += 1;} if (localDelete) {count -= 1;} const onLikeClick = () => { if (!currentUser) { - const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75; - showSignInDialog(offset); + showSignInDialog(); return; } if (currentUser.banned) { diff --git a/client/coral-settings/components/NotLoggedIn.js b/client/coral-settings/components/NotLoggedIn.js index c76553c50..a5d8b0693 100644 --- a/client/coral-settings/components/NotLoggedIn.js +++ b/client/coral-settings/components/NotLoggedIn.js @@ -1,17 +1,13 @@ import React from 'react'; import styles from './NotLoggedIn.css'; -import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; import translations from '../translations'; import I18n from 'coral-framework/modules/i18n/i18n'; const lang = new I18n(translations); -export default ({showSignInDialog, requireEmailConfirmation}) => ( +export default ({showSignInDialog}) => (
    -
    - { - showSignInDialog(); - }}>{lang.t('signIn')} {lang.t('toAccess')} + {lang.t('signIn')} {lang.t('toAccess')}
    {lang.t('fromSettingsPage')} diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index d057a9ed8..39e615725 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -12,7 +12,7 @@ import NotLoggedIn from '../components/NotLoggedIn'; import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'coral-plugin-history/CommentHistory'; -import {showSignInDialog} from 'coral-framework/actions/auth'; +import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth'; import translations from '../translations'; const lang = new I18n(translations); @@ -34,17 +34,17 @@ class ProfileContainer extends Component { } render() { - const {asset, showSignInDialog, data, myIgnoredUsersData, stopIgnoringUser} = this.props; + const {asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props; const {me} = this.props.data; - if (!me) { - return ; - } - if (data.loading) { return ; } + if (!me) { + return ; + } + const localProfile = this.props.user.profiles.find(p => p.provider === 'local'); const emailAddress = localProfile && localProfile.id; @@ -83,7 +83,6 @@ class ProfileContainer extends Component { :

    {lang.t('userNoComment')}

    } -
    ); } @@ -96,7 +95,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => - bindActionCreators({showSignInDialog}, dispatch); + bindActionCreators({showSignInDialog, checkLogin}, dispatch); export default compose( connect(mapStateToProps, mapDispatchToProps), diff --git a/client/coral-sign-in/components/CreateUsernameDialog.js b/client/coral-sign-in/components/CreateUsernameDialog.js index 4a7a99b81..b94b47c42 100644 --- a/client/coral-sign-in/components/CreateUsernameDialog.js +++ b/client/coral-sign-in/components/CreateUsernameDialog.js @@ -10,16 +10,12 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmitUsername, handleChange, ...props}) => { +const CreateUsernameDialog = ({open, handleClose, formData, handleSubmitUsername, handleChange, ...props}) => { return ( + open={open}> ×
    @@ -42,6 +38,7 @@ const CreateUsernameDialog = ({open, handleClose, offset, formData, handleSubmit
    this.emailInput = input} type="text" + style={{fontSize: 16}} id="email" name="email" />
    diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 6243472b4..ff2464f7c 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -6,15 +6,11 @@ import SignInContent from './SignInContent'; import SignUpContent from './SignUpContent'; import ForgotContent from './ForgotContent'; -const SignDialog = ({open, view, handleClose, offset, ...props}) => ( +const SignDialog = ({open, view, handleClose, ...props}) => ( + open={open}> × {view === 'SIGNIN' && } {view === 'SIGNUP' && } diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index a0ed5b5b4..7460efb97 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -20,8 +20,8 @@ const SignInContent = ({ }) => { return ( -
    -
    +
    +

    {auth.emailVerificationFailure ? lang.t('signIn.emailVerifyCTA') : lang.t('signIn.signIn')}

    @@ -42,7 +42,7 @@ const SignInContent = ({ {emailVerificationSuccess && } :
    -
    +
    @@ -58,6 +58,7 @@ const SignInContent = ({ type="email" label={lang.t('signIn.email')} value={formData.email} + style={{fontSize: 16}} onChange={handleChange} />
    @@ -80,7 +82,7 @@ const SignInContent = ({
    } -
    +
    changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')} {lang.t('signIn.needAnAccount')} diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index d127cc2c7..921f045be 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -83,6 +83,7 @@ class SignUpContent extends React.Component { type="email" label={lang.t('signIn.email')} value={formData.email} + style={{fontSize: 16}} showErrors={showErrors} errorMsg={errors.email} onChange={handleChange} @@ -93,6 +94,7 @@ class SignUpContent extends React.Component { label={lang.t('signIn.username')} value={formData.username} showErrors={showErrors} + style={{fontSize: 16}} errorMsg={errors.username} onChange={handleChange} /> @@ -102,6 +104,7 @@ class SignUpContent extends React.Component { label={lang.t('signIn.password')} value={formData.password} showErrors={showErrors} + style={{fontSize: 16}} errorMsg={errors.password} onChange={handleChange} minLength="8" @@ -112,6 +115,7 @@ class SignUpContent extends React.Component { type="password" label={lang.t('signIn.confirmPassword')} value={formData.confirmPassword} + style={{fontSize: 16}} showErrors={showErrors} errorMsg={errors.confirmPassword} onChange={handleChange} diff --git a/client/coral-sign-in/containers/ChangeUsernameContainer.js b/client/coral-sign-in/containers/ChangeUsernameContainer.js index c4450aa56..a3b4b969d 100644 --- a/client/coral-sign-in/containers/ChangeUsernameContainer.js +++ b/client/coral-sign-in/containers/ChangeUsernameContainer.js @@ -100,12 +100,11 @@ class ChangeUsernameContainer extends Component { } render() { - const {loggedIn, auth, offset} = this.props; + const {loggedIn, auth} = this.props; return (
    - {!noButton && } ({ fetchSignUpFacebook: () => dispatch(fetchSignUpFacebook()), fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)), requestConfirmEmail: (email, url) => dispatch(requestConfirmEmail(email, url)), - showSignInDialog: () => dispatch(showSignInDialog()), changeView: view => dispatch(changeView(view)), handleClose: () => dispatch(hideSignInDialog()), invalidForm: error => dispatch(invalidForm(error)), diff --git a/graph/hooks.js b/graph/hooks.js index c5b7541b3..7f6be7feb 100644 --- a/graph/hooks.js +++ b/graph/hooks.js @@ -53,6 +53,71 @@ const forEachField = (schema, fn) => { }); }; +/** + * Decorates the field with the post resolvers (if available) and attaches a + * default type in the form of `Default${typeName}`. + */ +const decorateResolveFunction = (field, typeName, fieldName, post) => { + + // Cache the original resolverType function. + let resolveType = field.resolveType; + + // defaultResolveType is the default type that is resolved on a resolver + // when the interface being looked up is not defined. + const defaultResolveType = `Default${typeName}`; + + // Return the function to handle the resolveType hooks. + const defaultResolveFn = (obj, context, info) => { + let type = resolveType(obj, context, info); + + // Only if a previous resolver was unable to resolve the field type do we + // progress to the hooks (in order!) to resolve the field name until we + // have resolved it. + if (typeof type !== 'undefined' && type != null) { + return type; + } + + // All else fails, resort to the defaultResolveType. + return defaultResolveType; + }; + + // This only needs to do something if post hooks are defined. + if (post.length === 0) { + + // Set the default on the resolveType function. + field.resolveType = defaultResolveFn; + + return; + } + + // Ensure it matches the format we expect. + Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`); + + // Return the function to handle the resolveType hooks. + field.resolveType = (obj, context, info) => { + let type = defaultResolveFn(obj, context, info); + + // Only if a previous resolver was unable to resolve the field type do we + // progress to the hooks (in order!) to resolve the field name until we + // have resolved it. + if (typeof type !== 'undefined' && type != null && type !== defaultResolveType) { + return type; + } + + // We will walk through the post hooks until we find the right one. This + // follows what redux does to combine existing reducers. + for (let i = 0; i < post.length; i++) { + let resolveType = post[i]; + let resolvedType = resolveType(obj, context, info); + if (typeof resolvedType !== 'undefined' && resolvedType != null) { + return resolvedType; + } + } + + return type; + }; +}; + /** * Decorates the schema with pre and post hooks as provided by the Plugin * Manager. @@ -115,11 +180,6 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa post: [] }); - // If we have no hooks to add here, don't try to modify anything. - if (pre.length === 0 && post.length === 0) { - return; - } - // If this is a resolve type, we need to do some specific things to handle // this type of field. if (isResolveType) { @@ -129,39 +189,13 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`); } - // This only needs to do something if post hooks are defined. - if (post.length === 0) { - return; - } - - // Ensure it matches the format we expect. - Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`); - - // Cache the original resolverType function. - let resolveType = field.resolveType; - - // Return the function to handle the resolveType hooks. - field.resolveType = (obj, context, info) => { - let type = resolveType(obj, context, info); - - // Only if a previous resolver was unable to resolve the field type do we - // progress to the hooks (in order!) to resolve the field name until we - // have resolved it. - if (typeof type !== 'undefined' && type != null) { - return type; - } - - // We will walk through the post hooks until we find the right one. This - // follows what redux does to combine existing reducers. - for (let i = 0; i < post.length; i++) { - let resolveType = post[i]; - type = resolveType(obj, context, info); - if (typeof type !== 'undefined' && type != null) { - return type; - } - } - }; + // Decorate the resolve function on the field with the new resolveType func. + decorateResolveFunction(field, typeName, fieldName, post); + return; + } + // If we have no hooks to add here, don't try to modify anything. + if (pre.length === 0 && post.length === 0) { return; } diff --git a/graph/resolvers/action_summary.js b/graph/resolvers/action_summary.js index 1986a0648..ac2154de8 100644 --- a/graph/resolvers/action_summary.js +++ b/graph/resolvers/action_summary.js @@ -8,7 +8,7 @@ const ActionSummary = { case 'DONTAGREE': return 'DontAgreeActionSummary'; } - }, + } }; module.exports = ActionSummary; diff --git a/graph/resolvers/flag_action.js b/graph/resolvers/flag_action.js index 80d1583ad..e4b30c408 100644 --- a/graph/resolvers/flag_action.js +++ b/graph/resolvers/flag_action.js @@ -8,7 +8,9 @@ const FlagAction = { return group_id; }, user({user_id}, _, {loaders: {Users}}) { - return Users.getByID.load(user_id); + if (user_id) { + return Users.getByID.load(user_id); + } }, }; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 8de1988ff..4dcb7ea11 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -84,9 +84,10 @@ const RootQuery = { }, myIgnoredUsers: async (_, args, {user, loaders: {Users}}) => { - if (user == null) { - return []; + if (!user) { + return null; } + // get currentUser again since context.user was out of date when running test/graph/mutations/ignoreUser const currentUser = (await Users.getByQuery({ids: [user.id], limit: 1}))[0]; if ( ! (currentUser && Array.isArray(currentUser.ignoresUsers) && currentUser.ignoresUsers.length)) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 0a067b49c..32c2c16c2 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -31,7 +31,7 @@ type User { username: String! # Action summaries against the user. - action_summaries: [ActionSummary] + action_summaries: [ActionSummary]! # Actions completed on the parent. actions: [Action] @@ -197,7 +197,7 @@ type Comment { actions: [Action] # Action summaries against a comment. - action_summaries: [ActionSummary] + action_summaries: [ActionSummary]! # The asset that a comment was made on. asset: Asset @@ -229,6 +229,22 @@ interface Action { created_at: Date } +# DefaultAction is the Action provided for undefined types. +type DefaultAction implements Action { + + # The ID of the action. + id: ID! + + # The author of the action. + user: User + + # The time when the Action was updated. + updated_at: Date + + # The time when the Action was created. + created_at: Date +} + # A summary of actions based on the specific grouping of the group_id. interface ActionSummary { @@ -239,6 +255,16 @@ interface ActionSummary { current_user: Action } +# DefaultActionSummary is the ActionSummary provided for undefined types. +type DefaultActionSummary implements ActionSummary { + + # The count of actions with this group. + count: Int + + # The current user's action. + current_user: Action +} + # A summary of actions for a specific action type on an Asset. interface AssetActionSummary { @@ -249,6 +275,16 @@ interface AssetActionSummary { actionableItemCount: Int } +# DefaultAssetActionSummary is the AssetActionSummary provided for undefined types. +type DefaultAssetActionSummary implements AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + # A summary of counts related to all the Flags on an Asset. type FlagAssetActionSummary implements AssetActionSummary { @@ -440,7 +476,7 @@ type Asset { # Summary of all Actions against all entities associated with the Asset. # (likes, flags, etc.). Requires the `ADMIN` role. - action_summaries: [AssetActionSummary] + action_summaries: [AssetActionSummary!] # The date that the asset was created. created_at: Date diff --git a/package.json b/package.json index 1b09c78c8..6c725a852 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,6 @@ "webpack": "^2.3.1" }, "engines": { - "node": "^7.9.0" + "node": "^7.8.0" } } diff --git a/plugins/coral-plugin-respect/client/components/RespectButton.js b/plugins/coral-plugin-respect/client/components/RespectButton.js index 9e851460e..c1e0c1b47 100644 --- a/plugins/coral-plugin-respect/client/components/RespectButton.js +++ b/plugins/coral-plugin-respect/client/components/RespectButton.js @@ -5,6 +5,7 @@ import Icon from './Icon'; import {I18n} from 'coral-framework'; import cn from 'classnames'; import translations from '../translations.json'; +import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; const lang = new I18n(translations); @@ -14,13 +15,11 @@ class RespectButton extends Component { const {postRespect, showSignInDialog, deleteAction, commentId} = this.props; const {me, comment} = this.props.data; - const respect = comment.action_summaries[0]; - const respected = (respect && respect.current_user); + const myRespectActionSummary = getMyActionSummary('RespectActionSummary', comment); // If the current user does not exist, trigger sign in dialog. if (!me) { - const offset = document.getElementById(`c_${commentId}`).getBoundingClientRect().top - 75; - showSignInDialog(offset); + showSignInDialog(); return; } @@ -29,29 +28,33 @@ class RespectButton extends Component { return; } - if (!respected) { + if (myRespectActionSummary) { + deleteAction(myRespectActionSummary.current_user.id); + } else { postRespect({ item_id: commentId, item_type: 'COMMENTS' }); - } else { - deleteAction(respect.current_user.id); } } render() { const {comment} = this.props.data; - const respect = comment && comment.action_summaries && comment.action_summaries[0]; - const respected = respect && respect.current_user; - let count = respect ? respect.count : 0; + + if (!comment) { + return null; + } + + const myRespect = getMyActionSummary('RespectActionSummary', comment); + let count = getTotalActionCount('RespectActionSummary', comment); return (
    @@ -64,4 +67,3 @@ RespectButton.propTypes = { }; export default RespectButton; - diff --git a/plugins/coral-plugin-respect/client/containers/RespectButton.js b/plugins/coral-plugin-respect/client/containers/RespectButton.js index 047f7991f..bcf0e342d 100644 --- a/plugins/coral-plugin-respect/client/containers/RespectButton.js +++ b/plugins/coral-plugin-respect/client/containers/RespectButton.js @@ -10,6 +10,8 @@ import RespectButton from '../components/RespectButton'; // See https://dev-blog.apollodata.com/apollo-clients-new-imperative-store-api-6cb69318a1e3 // and https://github.com/apollographql/apollo-client/issues/1224 +const isRespectAction = (a) => a.__typename === 'RespectActionSummary'; + export const RESPECT_QUERY = gql` query RespectQuery($commentId: ID!) { comment(id: $commentId) { @@ -52,18 +54,21 @@ const withDeleteAction = graphql(gql` }, updateQueries: { RespectQuery: (prev) => { - if (get(prev, 'comment.action_summaries.0.current_user.id') !== id) { + const action_summaries = prev.comment.action_summaries; + const idx = action_summaries.findIndex(isRespectAction); + if (idx < 0 || get(action_summaries[idx], 'current_user.id') !== id) { return prev; } const next = { ...prev, comment: { ...prev.comment, - action_summaries: [{ - __typename: 'RespectActionSummary', - count: prev.comment.action_summaries[0].count - 1, - current_user: null, - }], + action_summaries: action_summaries.map( + (a, i) => i !== idx ? a : ({ + ...a, + count: a.count - 1, + current_user: null, + })), } }; return next; @@ -102,21 +107,40 @@ const withPostRespect = graphql(gql` }, updateQueries: { RespectQuery: (prev, {mutationResult, queryVariables}) => { - if (queryVariables.commentId !== respect.item_id || - get(prev, 'comment.action_summaries.0.current_user')) { + if (queryVariables.commentId !== respect.item_id) { return prev; } + + let action_summaries = prev.comment.action_summaries; + let idx = action_summaries.findIndex(isRespectAction); + + // Check whether we already respected this comment. + if (idx >= 0 && action_summaries[idx].current_user) { + return prev; + } + + if (idx < 0) { + + // Add initial action when it doesn't exist. + action_summaries = action_summaries.concat([{ + __typename: 'RespectActionSummary', + count: 0, + current_user: null, + }]); + idx = action_summaries.length - 1; + } + const respectAction = mutationResult.data.createRespect.respect; - const count = prev.comment.action_summaries[0] ? prev.comment.action_summaries[0].count : 0; const next = { ...prev, comment: { ...prev.comment, - action_summaries: [{ - __typename: 'RespectActionSummary', - count: count + 1, - current_user: respectAction, - }], + action_summaries: action_summaries.map( + (a, i) => i !== idx ? a : ({ + ...a, + count: a.count + 1, + current_user: respectAction, + })), } }; return next; @@ -138,4 +162,3 @@ const enhance = compose( ); export default enhance(RespectButton); - diff --git a/services/actions.js b/services/actions.js index 0bcb81af0..40bef65bb 100644 --- a/services/actions.js +++ b/services/actions.js @@ -48,10 +48,16 @@ module.exports = class ActionsService { * Finds actions in an array of ids. * @param {String} ids array of user identifiers (uuid) */ - static findByItemIdArray(item_ids) { - return ActionModel.find({ + static async findByItemIdArray(item_ids) { + let actions = await ActionModel.find({ 'item_id': {$in: item_ids} }); + + if (actions === null) { + return []; + } + + return actions; } /** diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index 498090e32..d99d4bce1 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -1,6 +1,8 @@ + +