diff --git a/.gitignore b/.gitignore index f4014b561..e8dfd14e7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ plugins.json plugins/* !plugins/coral-plugin-facebook-auth !plugins/coral-plugin-respect +!plugins/coral-plugin-offtopic + **/node_modules/* 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 47e5674cb..43a9c513b 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 4f1cc2d13..53113558b 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -58,7 +58,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 09fab4a66..437cec3ea 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -21,12 +21,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..133a3790d --- /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 './Embed'; +import SignInContainer from 'coral-sign-in/containers/SignInContainer'; + +const routes = ( +
    + + +
    +); + +const AppRouter = () => ; + +export default AppRouter; diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 4b01e19a7..674c03ec5 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -19,14 +19,13 @@ 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 {Slot} from 'coral-framework'; +import Slot from 'coral-framework/components/Slot'; 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}` : ''; @@ -168,7 +174,7 @@ class Comment extends React.Component { ? : null } - + { (currentUser && (comment.user.id !== currentUser.id)) ? @@ -183,8 +189,10 @@ class Comment extends React.Component {
    + {/* TODO implmement iPerformedThisAction for the like */} - +
    @@ -217,7 +225,8 @@ class Comment extends React.Component { { if (!this.props.auth.user) { - const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75; - this.props.showSignInDialog(offset); + this.props.showSignInDialog(); } else { this.setState({activeReplyBox: reactKey}); } @@ -130,7 +129,7 @@ class Embed extends React.Component { const {activeTab} = this.state; const {closedAt, countCache = {}} = this.props.asset; const {asset, refetch, comment} = this.props.data; - const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth; + const {loggedIn, isAdmin, user, showSignInDialog} = this.props.auth; // even though the permalinked comment is the highlighted one, we're displaying its parent + replies const highlightedComment = comment && comment.parent ? comment.parent : comment; @@ -218,11 +217,10 @@ class Embed extends React.Component {
    :

    {asset.settings.closedMessage}

    } - {!loggedIn && } - {loggedIn && user && } + + {!loggedIn && } + + {loggedIn && user && } {loggedIn && } {/* the highlightedComment is isolated after the user followed a permalink */} @@ -290,7 +288,6 @@ class Embed extends React.Component { @@ -320,10 +317,12 @@ const mapDispatchToProps = dispatch => ({ addNotification: (type, text) => addNotification(type, text), clearNotification: () => dispatch(clearNotification()), editName: (username) => dispatch(editName(username)), - showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), + showSignInDialog: () => dispatch(showSignInDialog()), updateCountCache: (id, count) => dispatch(updateCountCache(id, count)), viewAllComments: () => dispatch(viewAllComments()), logout: () => dispatch(logout()), + openSignInPopUp: cb => dispatch(openSignInPopUp(cb)), + checkLogin: () => dispatch(checkLogin()), dispatch: d => dispatch(d), }); diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 3dc69400c..d7ee99e73 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -3,13 +3,15 @@ import {render} from 'react-dom'; import {ApolloProvider} from 'react-apollo'; import {client} from 'coral-framework/services/client'; -import store from 'coral-framework/services/store'; +import localStore from 'coral-framework/services/store'; -import Embed from './Embed'; +import AppRouter from './AppRouter'; + +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/actions/notification.js b/client/coral-framework/actions/notification.js index 37679972d..d77a3a16c 100644 --- a/client/coral-framework/actions/notification.js +++ b/client/coral-framework/actions/notification.js @@ -1,9 +1,12 @@ import pym from '../services/PymConnection'; +import * as actions from '../constants/notification'; export const addNotification = (notifType, text) => { pym.sendMessage('coral-alert', `${notifType}|${text}`); + return {type: actions.ADD_NOTIFICATION, notifType, text}; }; export const clearNotification = () => { pym.sendMessage('coral-clear-notification'); + return {type: actions.CLEAR_NOTIFICATION}; }; diff --git a/client/coral-framework/components/Slot.css b/client/coral-framework/components/Slot.css new file mode 100644 index 000000000..6048da181 --- /dev/null +++ b/client/coral-framework/components/Slot.css @@ -0,0 +1,3 @@ +.inline { + display: inline-block; +} diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 0e5cf86c2..c1e5dfe65 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -1,13 +1,14 @@ import React, {Component} from 'react'; import {getSlotElements} from 'coral-framework/helpers/plugins'; +import styles from './Slot.css'; class Slot extends Component { render() { - const {fill, ...rest} = this.props; + const {fill, inline = false, ...rest} = this.props; return ( - +
    {getSlotElements(fill, rest)} - +
    ); } } diff --git a/client/coral-framework/constants/notification.js b/client/coral-framework/constants/notification.js new file mode 100644 index 000000000..a7334119a --- /dev/null +++ b/client/coral-framework/constants/notification.js @@ -0,0 +1,2 @@ +export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; +export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js index da1603f9e..4a3abb48e 100644 --- a/client/coral-framework/graphql/mutations/index.js +++ b/client/coral-framework/graphql/mutations/index.js @@ -20,12 +20,11 @@ export const postComment = graphql(POST_COMMENT, { fragments: commentView }), props: ({ownProps, mutate}) => ({ - postItem: ({asset_id, body, parent_id}) => - mutate({ + postItem: comment => { + const {asset_id, body, parent_id, tags = []} = comment; + return mutate({ variables: { - asset_id, - body, - parent_id + comment }, optimisticResponse: { createComment: { @@ -39,14 +38,14 @@ export const postComment = graphql(POST_COMMENT, { parent_id, asset_id, action_summaries: [], - tags: [], + tags, status: null, id: 'pending' } } }, updateQueries: { - AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => { + AssetQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => { if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') { return oldData; @@ -62,8 +61,8 @@ export const postComment = graphql(POST_COMMENT, { ...oldData.asset, comments: oldData.asset.comments.map((oldComment) => { return oldComment.id === parent_id - ? {...oldComment, replies: [...oldComment.replies, comment]} - : oldComment; + ? {...oldComment, replies: [...oldComment.replies, comment]} + : oldComment; }) } }; @@ -83,7 +82,8 @@ export const postComment = graphql(POST_COMMENT, { return updatedAsset; } } - }) + }); + } }), }); diff --git a/client/coral-framework/graphql/mutations/postComment.graphql b/client/coral-framework/graphql/mutations/postComment.graphql index 110ab4b4e..f98558804 100644 --- a/client/coral-framework/graphql/mutations/postComment.graphql +++ b/client/coral-framework/graphql/mutations/postComment.graphql @@ -1,7 +1,7 @@ #import "../fragments/commentView.graphql" -mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) { - createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) { +mutation CreateComment ($comment: CreateCommentInput!) { + createComment(comment: $comment) { comment { ...commentView replyCount 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/reducers/index.js b/client/coral-framework/reducers/index.js index fae7e60d5..d5b66bbae 100644 --- a/client/coral-framework/reducers/index.js +++ b/client/coral-framework/reducers/index.js @@ -1,11 +1,13 @@ import auth from './auth'; import user from './user'; import asset from './asset'; +import {reducer as commentBox} from '../../coral-plugin-commentbox'; import {pluginReducers} from '../helpers/plugins'; export default { auth, user, asset, + commentBox, ...pluginReducers }; diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index fbc444eec..d7090f4b5 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -24,7 +24,7 @@ if (window.devToolsExtension) { middlewares.push(window.devToolsExtension()); } -export default createStore( +const store = createStore( combineReducers({ ...mainReducer, apollo: client.reducer() @@ -32,3 +32,6 @@ export default createStore( {}, compose(...middlewares) ); + +export default store; +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 19472ef70..2a988711d 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -2,56 +2,57 @@ 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/components/Slot'; +import {connect} from 'react-redux'; const name = 'coral-plugin-commentbox'; class CommentBox extends Component { - static propTypes = { - commentPostedHandler: PropTypes.func, - postItem: PropTypes.func.isRequired, - cancelButtonClicked: PropTypes.func, - assetId: PropTypes.string.isRequired, - parentId: PropTypes.string, - authorId: PropTypes.string.isRequired, - isReply: PropTypes.bool.isRequired, - canPost: PropTypes.bool, - currentUser: PropTypes.object - } + constructor(props) { + super(props); - state = { - body: '', - username: '' + this.state = { + username: '', + body: '', + hooks: { + preSubmit: [], + postSubmit: [] + } + }; } postComment = () => { const { - commentPostedHandler, - postItem, - assetId, - updateCountCache, isReply, - countCache, + assetId, parentId, + postItem, + countCache, addNotification, - authorId + updateCountCache, + commentPostedHandler } = this.props; let comment = { - body: this.state.body, asset_id: assetId, - author_id: authorId, - parent_id: parentId + parent_id: parentId, + body: this.state.body, + ...this.props.commentBox }; - if (this.props.charCount && this.state.body.length > this.props.charCount) { - return; - } !isReply && updateCountCache(assetId, countCache + 1); + + // Execute preSubmit Hooks + this.state.hooks.preSubmit.forEach(hook => hook()); + postItem(comment, 'comments') .then(({data}) => { const postedComment = data.createComment.comment; + // Execute postSubmit Hooks + this.state.hooks.postSubmit.forEach(hook => hook(data)); + if (postedComment.status === 'REJECTED') { addNotification('error', lang.t('comment-post-banned-word')); !isReply && updateCountCache(assetId, countCache); @@ -64,14 +65,67 @@ class CommentBox extends Component { commentPostedHandler(); } }) - .catch((err) => console.error(err)); + .catch((err) => console.error(err)); + this.setState({body: ''}); } + registerHook = (hookType = '', hook = () => {}) => { + if (typeof hook !== 'function') { + return console.warn(`Hooks must be functions. Please check your ${hookType} hooks`); + } else if (typeof hookType === 'string') { + this.setState(state => ({ + hooks: { + ...state.hooks, + [hookType]: [ + ...state.hooks[hookType], + hook + ] + } + })); + + return { + hookType, + hook + }; + + } else { + return console.warn('hookTypes must be a string. Please check your hooks'); + } + } + + unregisterHook = hookData => { + const {hookType, hook} = hookData; + + this.setState(state => { + let newHooks = state.hooks[newHooks]; + const idx = state.hooks[hookType].indexOf(hook); + + if (idx !== -1) { + newHooks = [ + ...state.hooks[hookType].slice(0, idx), + ...state.hooks[hookType].slice(idx + 1) + ]; + } + + return { + hooks: { + ...state.hooks, + [hookType]: newHooks + } + }; + + }); + } + + handleChange = e => this.setState({body: e.target.value}); + render () { const {styles, isReply, authorId, charCount} = this.props; let {cancelButtonClicked} = this.props; + const length = this.state.body.length; + const enablePostComment = !length || (charCount && length > charCount); if (isReply && typeof cancelButtonClicked !== 'function') { console.warn('the CommentBox component should have a cancelButtonClicked callback defined if it lives in a Reply'); @@ -93,33 +147,35 @@ class CommentBox extends Component { value={this.state.body} placeholder={lang.t('comment')} id={isReply ? 'replyText' : 'commentText'} - onChange={(e) => this.setState({body: e.target.value})} + onChange={this.handleChange} rows={3}/>
    charCount ? `${name}-char-max` : ''}`}> - { - charCount && - `${charCount - length} ${lang.t('characters-remaining')}` - } + {charCount && `${charCount - length} ${lang.t('characters-remaining')}`}
    + { isReply && ( ) } { authorId && ( ) @@ -129,6 +185,20 @@ class CommentBox extends Component { } } -export default CommentBox; +CommentBox.propTypes = { + commentPostedHandler: PropTypes.func, + postItem: PropTypes.func.isRequired, + cancelButtonClicked: PropTypes.func, + assetId: PropTypes.string.isRequired, + parentId: PropTypes.string, + authorId: PropTypes.string.isRequired, + isReply: PropTypes.bool.isRequired, + canPost: PropTypes.bool, + currentUser: PropTypes.object +}; + +const mapStateToProps = ({commentBox}) => ({commentBox}); + +export default connect(mapStateToProps, null)(CommentBox); const lang = new I18n(translations); diff --git a/client/coral-plugin-commentbox/actions.js b/client/coral-plugin-commentbox/actions.js new file mode 100644 index 000000000..46bcf4b1b --- /dev/null +++ b/client/coral-plugin-commentbox/actions.js @@ -0,0 +1,9 @@ +export const addTag = tag => ({ + type: 'ADD_TAG', + tag +}); + +export const removeTag = idx => ({ + type: 'REMOVE_TAG', + idx +}); diff --git a/client/coral-plugin-commentbox/constants.js b/client/coral-plugin-commentbox/constants.js new file mode 100644 index 000000000..b1290f02e --- /dev/null +++ b/client/coral-plugin-commentbox/constants.js @@ -0,0 +1,2 @@ +export const ADD_TAG = 'ADD_TAG'; +export const REMOVE_TAG = 'REMOVE_TAG'; diff --git a/client/coral-plugin-commentbox/index.js b/client/coral-plugin-commentbox/index.js new file mode 100644 index 000000000..9f166caeb --- /dev/null +++ b/client/coral-plugin-commentbox/index.js @@ -0,0 +1,5 @@ +import reducer from './reducer'; + +export default { + reducer +}; diff --git a/client/coral-plugin-commentbox/reducer.js b/client/coral-plugin-commentbox/reducer.js new file mode 100644 index 000000000..d9065fe87 --- /dev/null +++ b/client/coral-plugin-commentbox/reducer.js @@ -0,0 +1,25 @@ +import {ADD_TAG, REMOVE_TAG} from './constants'; + +const initialState = { + tags: [] +}; + +export default function commentBox (state = initialState, action) { + switch (action.type) { + case ADD_TAG : + return { + ...state, + tags: [...state.tags, action.tag] + }; + case REMOVE_TAG : + return { + ...state, + tags: [ + ...state.tags.slice(0, action.idx), + ...state.tags.slice(action.idx + 1) + ] + }; + default : + return state; + } +} diff --git a/client/coral-plugin-commentbox/styles.css b/client/coral-plugin-commentbox/styles.css new file mode 100644 index 000000000..d0f851f18 --- /dev/null +++ b/client/coral-plugin-commentbox/styles.css @@ -0,0 +1,6 @@ +.slot { + display: inline-block; + div { + display: inline-block; + } +} 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 2d756077d..553504462 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -2,6 +2,7 @@ import {connect} from 'react-redux'; import {compose} from 'react-apollo'; import React, {Component} from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; +import {bindActionCreators} from 'redux'; import {myCommentHistory, myIgnoredUsers} from 'coral-framework/graphql/queries'; import {stopIgnoringUser} from 'coral-framework/graphql/mutations'; @@ -12,6 +13,8 @@ import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'coral-plugin-history/CommentHistory'; +import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth'; + import translations from '../translations'; const lang = new I18n(translations); @@ -32,17 +35,17 @@ class ProfileContainer extends Component { } render() { - const {loggedIn, asset, showSignInDialog, data, myIgnoredUsersData, stopIgnoringUser} = this.props; + const {loggedIn, asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props; const {me} = this.props.data; - if (!loggedIn || !me) { - return ; - } - if (data.loading) { return ; } + if (!loggedIn || !me) { + return ; + } + const localProfile = this.props.user.profiles.find(p => p.provider === 'local'); const emailAddress = localProfile && localProfile.id; @@ -81,7 +84,6 @@ class ProfileContainer extends Component { :

    {lang.t('userNoComment')}

    } -
    ); } @@ -93,10 +95,8 @@ const mapStateToProps = state => ({ auth: state.auth.toJS() }); -const mapDispatchToProps = () => ({ - - // saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)) -}); +const mapDispatchToProps = 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/mutators/comment.js b/graph/mutators/comment.js index 728603ae2..982f7ade7 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -16,11 +16,14 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => { +const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => { - let tags = []; + // Building array of tags + tags = tags.map(tag => ({name: tag})); + + // If admin or moderator, adding STAFF tag if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags = [{name: 'STAFF'}]; + tags.push({name: 'STAFF'}); } return CommentsService.publicCreate({ 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/root_mutation.js b/graph/resolvers/root_mutation.js index 38183bff6..661cc9f96 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -2,8 +2,8 @@ const wrapResponse = require('../helpers/response'); const CommentsService = require('../../services/comments'); const RootMutation = { - createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.create({asset_id, parent_id, body})); + createComment(_, {comment}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.create(comment)); }, createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) { return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'})); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index b26ecabca..4dcb7ea11 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -84,6 +84,9 @@ const RootQuery = { }, myIgnoredUsers: async (_, args, {user, loaders: {Users}}) => { + 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]; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index ef6ff5937..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 @@ -602,6 +638,26 @@ input CreateLikeInput { item_type: ACTION_ITEM_TYPE! } +enum TAG_TYPE { + STAFF +} + +input CreateCommentInput { + + # The asset id + asset_id: ID! + + # The id of the parent comment + parent_id: ID + + # The body of the comment + body: String! + + # Tags + tags: [TAG_TYPE] + +} + type CreateLikeResponse implements Response { # The like that was created. @@ -728,7 +784,7 @@ type StopIgnoringUserResponse implements Response { type RootMutation { # Creates a comment on the asset. - createComment(asset_id: ID!, parent_id: ID, body: String!): CreateCommentResponse + createComment(comment: CreateCommentInput!): CreateCommentResponse # Creates a like on an entity. createLike(like: CreateLikeInput!): CreateLikeResponse diff --git a/out.gif b/out.gif new file mode 100644 index 000000000..e69de29bb 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-offtopic/client/.babelrc b/plugins/coral-plugin-offtopic/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/coral-plugin-offtopic/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/coral-plugin-offtopic/client/.eslintrc.json b/plugins/coral-plugin-offtopic/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/coral-plugin-offtopic/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/coral-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js new file mode 100644 index 000000000..4e7ef7adf --- /dev/null +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js @@ -0,0 +1,38 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {addTag, removeTag} from 'coral-plugin-commentbox/actions'; +import styles from './styles.css'; + +class OffTopicCheckbox extends React.Component { + + label = 'OFF_TOPIC'; + + handleChange = (e) => { + if (e.target.checked) { + this.props.addTag(this.label) + } else { + const idx = this.props.commentBox.tags.indexOf(this.label); + this.props.removeTag(idx); + } + } + + render() { + return ( +
    + +
    + ) + } +} + + +const mapStateToProps = ({commentBox}) => ({commentBox}); + +const mapDispatchToProps = dispatch => + bindActionCreators({addTag, removeTag}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox); diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js new file mode 100644 index 000000000..73e2372ed --- /dev/null +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js @@ -0,0 +1,18 @@ +import React from 'react'; +import styles from './styles.css'; + +const isOffTopic = (tags) => { + return !!tags.filter(tag => tag.name === 'OFF_TOPIC').length +} + +export default (props) => ( + + { + isOffTopic(props.comment.tags) ? ( + + Off-topic + + ) : null + } + +); diff --git a/plugins/coral-plugin-offtopic/client/components/styles.css b/plugins/coral-plugin-offtopic/client/components/styles.css new file mode 100644 index 000000000..f3e290b09 --- /dev/null +++ b/plugins/coral-plugin-offtopic/client/components/styles.css @@ -0,0 +1,18 @@ +.offTopic { + height: 100%; +} + +.offTopicLabel { + padding: 10px 20px; + display: block; +} + +.tag { + background: rgba(245, 188, 168, 0.6); + display: inline-block; + margin: 0px 5px; + padding: 5px 5px; + border-radius: 2px; +} + + diff --git a/plugins/coral-plugin-offtopic/client/index.js b/plugins/coral-plugin-offtopic/client/index.js new file mode 100644 index 000000000..c006c9a9e --- /dev/null +++ b/plugins/coral-plugin-offtopic/client/index.js @@ -0,0 +1,9 @@ +import OffTopicCheckbox from './components/OffTopicCheckbox'; +import OffTopicTag from './components/OffTopicTag'; + +export default { + slots: { + commentBoxDetail: [OffTopicCheckbox], + commentInfoBar: [OffTopicTag] + } +}; diff --git a/plugins/coral-plugin-offtopic/index.js b/plugins/coral-plugin-offtopic/index.js new file mode 100644 index 000000000..51ad1c24e --- /dev/null +++ b/plugins/coral-plugin-offtopic/index.js @@ -0,0 +1,6 @@ +const {readFileSync} = require('fs'); +const path = require('path'); + +module.exports = { + typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8') +}; diff --git a/plugins/coral-plugin-offtopic/server/typeDefs.graphql b/plugins/coral-plugin-offtopic/server/typeDefs.graphql new file mode 100644 index 000000000..48ba577cf --- /dev/null +++ b/plugins/coral-plugin-offtopic/server/typeDefs.graphql @@ -0,0 +1,4 @@ +## Extending TAG_TYPE by adding OFF_TOPIC Tag +enum TAG_TYPE { + OFF_TOPIC +} 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/plugins/coral-plugin-respect/client/index.js b/plugins/coral-plugin-respect/client/index.js index 2caa6611b..a336786d6 100644 --- a/plugins/coral-plugin-respect/client/index.js +++ b/plugins/coral-plugin-respect/client/index.js @@ -1,4 +1,5 @@ import RespectButton from './containers/RespectButton'; + export default { slots: { commentDetail: [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/services/comments.js b/services/comments.js index 75b53873a..7e9cca20f 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,10 +3,10 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const ALLOWED_TAGS = [ - {name: 'STAFF'}, - {name: 'BEST'}, -]; +// const ALLOWED_TAGS = [ +// {name: 'STAFF'}, +// {name: 'BEST'}, +// ]; const STATUSES = [ 'ACCEPTED', @@ -53,9 +53,10 @@ module.exports = class CommentsService { */ static addTag(id, name, assigned_by) { - if (ALLOWED_TAGS.find((t) => t.name === name) == null) { - return Promise.reject(new Error('tag not allowed')); - } + // Disabling allowed tags until we are able to extend them + // if (ALLOWED_TAGS.find((t) => t.name === name) == null) { + // return Promise.reject(new Error('tag not allowed')); + // } const filter = { id, diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index b3bdd459a..cf8481094 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -12,8 +12,8 @@ describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); const query = ` - mutation CreateComment($body: String = "Here's my comment!") { - createComment(asset_id: "123", body: $body) { + mutation CreateComment($comment: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) { + createComment(comment: $comment) { comment { id status @@ -170,7 +170,10 @@ describe('graph.mutations.createComment', () => { const context = new Context({user: new UserModel({status: 'ACTIVE'})}); return graphql(schema, query, {}, context, { - body + comment: { + asset_id: '123', + body + } }) .then(({data, errors}) => { expect(errors).to.be.undefined; diff --git a/test/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js similarity index 93% rename from test/graph/mutations/ignoreUser.js rename to test/server/graph/mutations/ignoreUser.js index c5633bfb4..54d6305a1 100644 --- a/test/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -1,10 +1,10 @@ const expect = require('chai').expect; const {graphql} = require('graphql'); -const schema = require('../../../graph/schema'); -const Context = require('../../../graph/context'); -const UsersService = require('../../../services/users'); -const SettingsService = require('../../../services/settings'); +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UsersService = require('../../../../services/users'); +const SettingsService = require('../../../../services/settings'); const ignoreUserMutation = ` mutation ignoreUser ($id: ID!) { @@ -94,7 +94,7 @@ describe('graph.mutations.stopIgnoringUser', () => { if (response.errors && response.errors.length) { console.error(response.errors); } - expect(response.errors).to.be.empty; + expect(response.errors).to.be.empty; }); const stopIgnoringUserMutation = ` @@ -112,7 +112,7 @@ describe('graph.mutations.stopIgnoringUser', () => { if (stopIgnoringUserResponse.errors && stopIgnoringUserResponse.errors.length) { console.error(stopIgnoringUserResponse.errors); } - expect(stopIgnoringUserResponse.errors).to.be.empty; + expect(stopIgnoringUserResponse.errors).to.be.empty; // now check my ignored users const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); diff --git a/test/graph/queries/asset.js b/test/server/graph/queries/asset.js similarity index 88% rename from test/graph/queries/asset.js rename to test/server/graph/queries/asset.js index ba31fe330..79abeda0e 100644 --- a/test/graph/queries/asset.js +++ b/test/server/graph/queries/asset.js @@ -1,12 +1,12 @@ const expect = require('chai').expect; const {graphql} = require('graphql'); -const schema = require('../../../graph/schema'); -const Context = require('../../../graph/context'); -const UsersService = require('../../../services/users'); -const SettingsService = require('../../../services/settings'); -const Asset = require('../../../models/asset'); -const CommentsService = require('../../../services/comments'); +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UsersService = require('../../../../services/users'); +const SettingsService = require('../../../../services/settings'); +const Asset = require('../../../../models/asset'); +const CommentsService = require('../../../../services/comments'); describe('graph.queries.asset', () => { beforeEach(async () => { @@ -87,7 +87,7 @@ describe('graph.queries.asset', () => { `; const assetCommentsResponse = await graphql(schema, assetCommentsWithoutIgnoredQuery, {}, context, {assetId, assetUrl, excludeIgnored: true}); const comments = assetCommentsResponse.data.asset.comments; - expect(comments.length).to.equal(2); + expect(comments.length).to.equal(2); }); }); 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 @@ + +