diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 2bd59b79d..bfb2e197c 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -39,6 +39,9 @@ const routes = ( {/* Moderation Routes */} + + + diff --git a/client/coral-admin/src/components/ActionButton.js b/client/coral-admin/src/components/ActionButton.js index 3bd96abad..1fcafeac0 100644 --- a/client/coral-admin/src/components/ActionButton.js +++ b/client/coral-admin/src/components/ActionButton.js @@ -1,17 +1,24 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import styles from './ModerationList.css'; import {Button} from 'coral-ui'; import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap'; -const ActionButton = ({type = '', ...props}) => { +const ActionButton = ({type = '', status, ...props}) => { + const typeName = type.toLowerCase(); + const active = ((type === 'REJECT' && status === 'REJECTED') || (type === 'APPROVE' && status === 'ACCEPTED')); + return ( ); }; +ActionButton.propTypes = { + status: PropTypes.string +}; + export default ActionButton; diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css index fc2ba9931..1e871f97c 100644 --- a/client/coral-admin/src/components/ModerationList.css +++ b/client/coral-admin/src/components/ModerationList.css @@ -188,3 +188,15 @@ margin: 0; width: 140px; } + +.approve__active { + box-shadow: none; + color: white; + background-color: #519954; +} + +.reject__active, .rejected__active { + color: white; + background-color: #D03235; + box-shadow: none; +} diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 2c556f36f..dffa4de26 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -135,6 +135,9 @@ class ModerationContainer extends Component { const comments = data[activeTab]; let activeTabCount; switch(activeTab) { + case 'all': + activeTabCount = data.allCount; + break; case 'premod': activeTabCount = data.premodCount; break; @@ -151,6 +154,7 @@ class ModerationContainer extends Component { { const linkText = links ? links.map(link => link.raw) : []; const flagActionSummaries = getActionSummary('FlagActionSummary', comment); const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction'); + let commentType = ''; + if (comment.status === 'PREMOD') { + commentType = 'premod'; + } else if (flagActions && flagActions.length) { + commentType = 'flagged'; + } return (
  • @@ -36,7 +42,7 @@ const Comment = ({actions = [], comment, ...props}) => { {timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} /> - + {comment.user.status === 'banned' ? @@ -64,6 +70,7 @@ const Comment = ({actions = [], comment, ...props}) => { props.acceptComment({commentId: comment.id})} rejectComment={() => props.rejectComment({commentId: comment.id})} /> diff --git a/client/coral-admin/src/containers/ModerationQueue/components/LoadMore.js b/client/coral-admin/src/containers/ModerationQueue/components/LoadMore.js index f8d3f0ed4..71b6b9e37 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/LoadMore.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/LoadMore.js @@ -23,7 +23,7 @@ LoadMore.propTypes = { comments: PropTypes.array.isRequired, loadMore: PropTypes.func.isRequired, sort: PropTypes.oneOf(['CHRONOLOGICAL', 'REVERSE_CHRONOLOGICAL']).isRequired, - tab: PropTypes.oneOf(['rejected', 'premod', 'flagged']).isRequired, + tab: PropTypes.oneOf(['rejected', 'premod', 'flagged', 'all']).isRequired, assetId: PropTypes.string, showLoadMore: PropTypes.bool.isRequired }; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js index 0606fdb81..032aa740e 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js @@ -4,22 +4,18 @@ import styles from './styles.css'; import {SelectField, Option} from 'react-mdl-selectfield'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-admin/src/translations.json'; +import {Icon} from 'coral-ui'; import {Link} from 'react-router'; const lang = new I18n(translations); const ModerationMenu = ( - {asset, premodCount, rejectedCount, flaggedCount, selectSort, sort} + {asset, allCount, premodCount, rejectedCount, flaggedCount, selectSort, sort} ) => { - const premodPath = asset - ? `/admin/moderate/premod/${asset.id}` - : '/admin/moderate/premod'; - const rejectPath = asset - ? `/admin/moderate/rejected/${asset.id}` - : '/admin/moderate/rejected'; - const flagPath = asset - ? `/admin/moderate/flagged/${asset.id}` - : '/admin/moderate/flagged'; + + function getPath (type) { + return asset ? `/admin/moderate/${type}/${asset.id}` : `/admin/moderate/${type}`; + } return (
    @@ -27,22 +23,28 @@ const ModerationMenu = (
    - {lang.t('modqueue.premod')} + {lang.t('modqueue.all')} - {lang.t('modqueue.flagged')} + {lang.t('modqueue.premod')} - {lang.t('modqueue.rejected')} + {lang.t('modqueue.flagged')} + + + {lang.t('modqueue.rejected')}
    ({limit, cursor, sort, tab, asset_id}) => { let statuses; switch(tab) { + case 'all': + statuses = null; + break; case 'premod': statuses = ['PREMOD']; break; diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql index da8760be9..f124b87b5 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -1,6 +1,13 @@ #import "../fragments/commentView.graphql" query ModQueue ($asset_id: ID, $sort: SORT_ORDER) { + all: comments(query: { + statuses: [NONE, PREMOD, ACCEPTED, REJECTED], + asset_id: $asset_id, + sort: $sort + }) { + ...commentView + } premod: comments(query: { statuses: [PREMOD], asset_id: $asset_id, @@ -28,6 +35,9 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) { title url } + allCount: commentCount(query: { + asset_id: $asset_id + }) premodCount: commentCount(query: { statuses: [PREMOD], asset_id: $asset_id diff --git a/client/coral-embed-stream/src/AppRouter.js b/client/coral-embed-stream/src/AppRouter.js index 133a3790d..e2553889c 100644 --- a/client/coral-embed-stream/src/AppRouter.js +++ b/client/coral-embed-stream/src/AppRouter.js @@ -1,7 +1,7 @@ import React from 'react'; import {Router, Route, browserHistory} from 'react-router'; -import Embed from './Embed'; +import Embed from './containers/Embed'; import SignInContainer from 'coral-sign-in/containers/SignInContainer'; const routes = ( diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js deleted file mode 100644 index fbe24c8c7..000000000 --- a/client/coral-embed-stream/src/Embed.js +++ /dev/null @@ -1,348 +0,0 @@ -import React from 'react'; -import {compose} from 'react-apollo'; -import {connect} from 'react-redux'; -import isEqual from 'lodash/isEqual'; -import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from 'coral-framework/translations'; -const lang = new I18n(translations); - -import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui'; - -const {logout, showSignInDialog, requestConfirmEmail, openSignInPopUp, checkLogin} = authActions; -const {addNotification, clearNotification} = notificationActions; -const {fetchAssetSuccess} = assetActions; -import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments'; - -import {queryStream} from 'coral-framework/graphql/queries'; -import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; -import {editName} from 'coral-framework/actions/user'; -import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset'; -import {notificationActions, authActions, assetActions, pym} from 'coral-framework'; - -import Stream from './Stream'; -import InfoBox from 'coral-plugin-infobox/InfoBox'; -import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; -import {ModerationLink} from 'coral-plugin-moderation'; -import Count from 'coral-plugin-comment-count/CommentCount'; -import CommentBox from 'coral-plugin-commentbox/CommentBox'; -import UserBox from 'coral-sign-in/components/UserBox'; -import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; -import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUsernameContainer'; -import ProfileContainer from 'coral-settings/containers/ProfileContainer'; -import RestrictedContent from 'coral-framework/components/RestrictedContent'; -import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; -import HighlightedComment from './Comment'; -import LoadMore from './LoadMore'; -import NewCount from './NewCount'; - -class Embed extends React.Component { - - constructor(props) { - super(props); - this.state = { - activeTab: 0, - showSignInDialog: false, - activeReplyBox: '' - }; - } - - changeTab = (tab) => { - - // Everytime the comes from another tab, the Stream needs to be updated. - if (tab === 0) { - this.props.viewAllComments(); - this.props.data.refetch(); - } - - this.setState({ - activeTab: tab - }); - } - - static propTypes = { - data: React.PropTypes.shape({ - loading: React.PropTypes.bool, - error: React.PropTypes.object - }).isRequired, - - // dispatch action to add a tag to a comment - addCommentTag: React.PropTypes.func, - - // dispatch action to remove a tag from a comment - removeCommentTag: React.PropTypes.func, - - // dispatch action to ignore another user - ignoreUser: React.PropTypes.func, - } - - componentDidMount () { - pym.sendMessage('childReady'); - this.props.checkLogin(); - } - - componentWillUnmount () { - clearInterval(this.state.countPoll); - } - - componentWillReceiveProps (nextProps) { - const {loadAsset} = this.props; - if(!isEqual(nextProps.data.asset, this.props.data.asset)) { - loadAsset(nextProps.data.asset); - - const {getCounts, updateCountCache, asset: {countCache}} = this.props; - const {asset} = nextProps.data; - - if (!countCache) { - updateCountCache(asset.id, asset.commentCount); - } - - this.setState({ - countPoll: setInterval(() => { - const {asset} = this.props.data; - getCounts({ - asset_id: asset.id, - limit: asset.comments.length, - sort: 'REVERSE_CHRONOLOGICAL' - }); - }, NEW_COMMENT_COUNT_POLL_INTERVAL) - }); - } - } - - componentDidUpdate(prevProps) { - if(!isEqual(prevProps.data.comment, this.props.data.comment)) { - - // Scroll to a permalinked comment if one is in the URL once the page is done rendering. - setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0); - } - } - - setActiveReplyBox = (reactKey) => { - if (!this.props.auth.user) { - this.props.showSignInDialog(); - } else { - this.setState({activeReplyBox: reactKey}); - } - } - - render () { - const {activeTab} = this.state; - const {closedAt, countCache = {}} = this.props.asset; - const {asset, refetch, comment} = this.props.data; - 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; - - const openStream = closedAt === null; - - const banned = user && user.status === 'BANNED'; - - const hasOlderComments = !!( - asset && - asset.lastComment && - asset.lastComment.id !== asset.comments[asset.comments.length - 1].id - ); - - const expandForLogin = showSignInDialog ? { - minHeight: document.body.scrollHeight + 200 - } : {}; - - if (!asset) { - return ; - } - - // Find the created_at date of the first comment. If no comments exist, set the date to a week ago. - const firstCommentDate = asset.comments[0] - ? asset.comments[0].created_at - : new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(); - - const userBox = this.props.logout().then(refetch)} changeTab={this.changeTab}/>; - - // TODO: This is a quickfix and will be replaced after our refactor. - const ignoredUsers = this.props.userData.ignoredUsers; - const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id); - - return ( -
    -
    - - - {lang.t('myProfile')} - Configure Stream - - { - highlightedComment && - - } - - { loggedIn ? userBox : null } - { - openStream - ?
    - - - - }> - { - user - ? - : null - } - -
    - :

    {asset.settings.closedMessage}

    - } - - {!loggedIn && } - - {loggedIn && user && } - {loggedIn && } - - {/* the highlightedComment is isolated after the user followed a permalink */} - { - highlightedComment - ? - :
    - -
    - -
    - -
    - } -
    - - - - - - { loggedIn ? userBox : null } - - - -
    -
    - ); - } -} - -const mapStateToProps = state => ({ - auth: state.auth.toJS(), - userData: state.user.toJS(), - asset: state.asset.toJS(), -}); - -const mapDispatchToProps = dispatch => ({ - requestConfirmEmail: () => dispatch(requestConfirmEmail()), - loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)), - addNotification: (type, text) => addNotification(type, text), - clearNotification: () => dispatch(clearNotification()), - editName: (username) => dispatch(editName(username)), - 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), -}); - -export default compose( - connect(mapStateToProps, mapDispatchToProps), - postComment, - postFlag, - postLike, - postDontAgree, - addCommentTag, - removeCommentTag, - ignoreUser, - deleteAction, - queryStream, -)(Embed); diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js deleted file mode 100644 index ab64ee98f..000000000 --- a/client/coral-embed-stream/src/Stream.js +++ /dev/null @@ -1,103 +0,0 @@ -import React, {PropTypes} from 'react'; -import Comment from './Comment'; -import IgnoredCommentTombstone from './IgnoredCommentTombstone'; - -class Stream extends React.Component { - - static propTypes = { - addNotification: PropTypes.func.isRequired, - postItem: PropTypes.func.isRequired, - asset: PropTypes.object.isRequired, - open: PropTypes.bool.isRequired, - comments: PropTypes.array.isRequired, - currentUser: PropTypes.shape({ - username: PropTypes.string, - id: PropTypes.string - }), - - charCountEnable: PropTypes.bool.isRequired, - maxCharCount: PropTypes.number, - - // dispatch action to add a tag to a comment - addCommentTag: PropTypes.func, - - // dispatch action to remove a tag from a comment - removeCommentTag: PropTypes.func, - - // dispatch action to ignore another user - ignoreUser: React.PropTypes.func, - - // list of user ids that should be rendered as ignored - ignoredUsers: React.PropTypes.arrayOf(React.PropTypes.string) - } - - constructor(props) { - super(props); - this.state = {activeReplyBox: '', countPoll: null}; - } - - render () { - const { - comments, - currentUser, - asset, - postItem, - addNotification, - postFlag, - postLike, - open, - postDontAgree, - loadMore, - deleteAction, - showSignInDialog, - addCommentTag, - removeCommentTag, - pluginProps, - ignoreUser, - ignoredUsers, - charCountEnable, - maxCharCount, - } = this.props; - const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id); - return ( -
    - { - comments.map(comment => - commentIsIgnored(comment) - ? - : - ) - } -
    - ); - } -} - -export default Stream; diff --git a/client/coral-embed-stream/src/actions/embed.js b/client/coral-embed-stream/src/actions/embed.js new file mode 100644 index 000000000..863494c68 --- /dev/null +++ b/client/coral-embed-stream/src/actions/embed.js @@ -0,0 +1,10 @@ +import * as actions from '../constants/embed'; +import {viewAllComments} from './stream'; + +export const setActiveTab = (tab) => (dispatch, getState) => { + dispatch({type: actions.SET_ACTIVE_TAB, tab}); + if (getState().stream.commentId) { + dispatch(viewAllComments()); + } +}; + diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js new file mode 100644 index 000000000..81b57a0fe --- /dev/null +++ b/client/coral-embed-stream/src/actions/stream.js @@ -0,0 +1,40 @@ +import {pym} from 'coral-framework'; +import * as actions from '../constants/stream'; + +export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); +export const setCommentCountCache = (amount) => ({type: actions.SET_COMMENT_COUNT_CACHE, amount}); + +function removeParam(key, sourceURL) { + let rtn = sourceURL.split('?')[0]; + let param; + let params_arr = []; + let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : ''; + if (queryString !== '') { + params_arr = queryString.split('&'); + for (let i = params_arr.length - 1; i >= 0; i -= 1) { + param = params_arr[i].split('=')[0]; + if (param === key) { + params_arr.splice(i, 1); + } + } + rtn = `${rtn}?${params_arr.join('&')}`; + } + return rtn; +} + +export const viewAllComments = () => { + + // remove the comment_id url param + const modifiedUrl = removeParam('comment_id', location.href); + + try { + + // "window" here refers to the embedded iframe + window.history.replaceState({}, document.title, modifiedUrl); + + // also change the parent url + pym.sendMessage('coral-view-all-comments'); + } catch (e) { /* not sure if we're worried about old browsers */ } + + return {type: actions.VIEW_ALL_COMMENTS}; +}; diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/components/Comment.css similarity index 100% rename from client/coral-embed-stream/src/Comment.css rename to client/coral-embed-stream/src/components/Comment.css diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/components/Comment.js similarity index 94% rename from client/coral-embed-stream/src/Comment.js rename to client/coral-embed-stream/src/components/Comment.js index 63b3ef8dc..4f8426159 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -18,8 +18,8 @@ import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import FlagComment from 'coral-plugin-flags/FlagComment'; import LikeButton from 'coral-plugin-likes/LikeButton'; import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton'; -import LoadMore from 'coral-embed-stream/src/LoadMore'; import 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'; @@ -178,8 +178,14 @@ class Comment extends React.Component { ? : null } - - + { (currentUser && (comment.user.id !== currentUser.id)) ? - +
    @@ -263,6 +276,8 @@ class Comment extends React.Component { return commentIsIgnored(reply) ? : { + switch(tab) { + case 0: + this.props.setActiveTab('stream'); + break; + case 1: + this.props.setActiveTab('profile'); + + // TODO: move data fetching to profile container. + this.props.data.refetch(); + break; + case 2: + this.props.setActiveTab('config'); + + // TODO: move data fetching to config container. + this.props.data.refetch(); + break; + } + } + + render () { + const {activeTab, logout, viewAllComments, commentId} = this.props; + const {asset: {totalCommentCount}} = this.props.root; + const {loggedIn, isAdmin, user} = this.props.auth; + + const userBox = ; + + return ( +
    +
    + + + {lang.t('myProfile')} + Configure Stream + + { + commentId && + + } + + { loggedIn ? userBox : null } + + + + + + + + { loggedIn ? userBox : null } + + + +
    +
    + ); + } +} + +Embed.propTypes = { + data: React.PropTypes.shape({ + loading: React.PropTypes.bool, + error: React.PropTypes.object + }).isRequired, +}; diff --git a/client/coral-embed-stream/src/IgnoreUserWizard.css b/client/coral-embed-stream/src/components/IgnoreUserWizard.css similarity index 100% rename from client/coral-embed-stream/src/IgnoreUserWizard.css rename to client/coral-embed-stream/src/components/IgnoreUserWizard.css diff --git a/client/coral-embed-stream/src/IgnoreUserWizard.js b/client/coral-embed-stream/src/components/IgnoreUserWizard.js similarity index 100% rename from client/coral-embed-stream/src/IgnoreUserWizard.js rename to client/coral-embed-stream/src/components/IgnoreUserWizard.js diff --git a/client/coral-embed-stream/src/IgnoredCommentTombstone.js b/client/coral-embed-stream/src/components/IgnoredCommentTombstone.js similarity index 100% rename from client/coral-embed-stream/src/IgnoredCommentTombstone.js rename to client/coral-embed-stream/src/components/IgnoredCommentTombstone.js diff --git a/client/coral-embed-stream/src/LoadMore.js b/client/coral-embed-stream/src/components/LoadMore.js similarity index 95% rename from client/coral-embed-stream/src/LoadMore.js rename to client/coral-embed-stream/src/components/LoadMore.js index 1a58c8d19..8c665a2db 100644 --- a/client/coral-embed-stream/src/LoadMore.js +++ b/client/coral-embed-stream/src/components/LoadMore.js @@ -1,7 +1,7 @@ import React, {PropTypes} from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations.json'; -import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments'; +import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream'; import {Button} from 'coral-ui'; const lang = new I18n(translations); diff --git a/client/coral-embed-stream/src/NewCount.js b/client/coral-embed-stream/src/components/NewCount.js similarity index 80% rename from client/coral-embed-stream/src/NewCount.js rename to client/coral-embed-stream/src/components/NewCount.js index 8548e6a14..a006ee802 100644 --- a/client/coral-embed-stream/src/NewCount.js +++ b/client/coral-embed-stream/src/components/NewCount.js @@ -3,9 +3,9 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations.json'; const lang = new I18n(translations); -const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, updateCountCache}) => (e) => { +const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, setCommentCountCache}) => (e) => { e.preventDefault(); - updateCountCache(assetId, commentCount); + setCommentCountCache(commentCount); loadMore({ asset_id: assetId, limit: 500, @@ -15,11 +15,11 @@ const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, upd }; const NewCount = (props) => { - const newComments = props.commentCount - props.countCache; + const newComments = props.commentCount - props.commentCountCache; return
    { - props.countCache && newComments > 0 ? + props.commentCountCache && newComments > 0 ? } + {loggedIn && user && } + {loggedIn && } + + {/* the highlightedComment is isolated after the user followed a permalink */} + { + highlightedComment + ? + :
    + +
    + { + comments.map(comment => + commentIsIgnored(comment) + ? + : + ) + } +
    + +
    + } +
    + ); + } +} + +Stream.propTypes = { + addNotification: PropTypes.func.isRequired, + postItem: PropTypes.func.isRequired, + + // dispatch action to add a tag to a comment + addCommentTag: PropTypes.func, + + // dispatch action to remove a tag from a comment + removeCommentTag: PropTypes.func, + + // dispatch action to ignore another user + ignoreUser: React.PropTypes.func, +}; + +export default Stream; diff --git a/client/coral-embed-stream/src/TopRightMenu.css b/client/coral-embed-stream/src/components/TopRightMenu.css similarity index 100% rename from client/coral-embed-stream/src/TopRightMenu.css rename to client/coral-embed-stream/src/components/TopRightMenu.css diff --git a/client/coral-embed-stream/src/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js similarity index 100% rename from client/coral-embed-stream/src/TopRightMenu.js rename to client/coral-embed-stream/src/components/TopRightMenu.js diff --git a/client/coral-embed-stream/src/constants/embed.js b/client/coral-embed-stream/src/constants/embed.js new file mode 100644 index 000000000..178e5d6e4 --- /dev/null +++ b/client/coral-embed-stream/src/constants/embed.js @@ -0,0 +1 @@ +export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB'; diff --git a/client/coral-embed-stream/src/constants/stream.js b/client/coral-embed-stream/src/constants/stream.js new file mode 100644 index 000000000..cb17edb2f --- /dev/null +++ b/client/coral-embed-stream/src/constants/stream.js @@ -0,0 +1,5 @@ +export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX'; +export const SET_COMMENT_COUNT_CACHE = 'SET_COMMENT_COUNT_CACHE'; +export const ADDTL_COMMENTS_ON_LOAD_MORE = 10; +export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000; +export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS'; diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js new file mode 100644 index 000000000..524f7ec62 --- /dev/null +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -0,0 +1,40 @@ +import {gql} from 'react-apollo'; +import Comment from '../components/Comment'; +import withFragments from 'coral-framework/hocs/withFragments'; +import {getSlotsFragments} from 'coral-framework/helpers/plugins'; + +const pluginFragments = getSlotsFragments(['commentInfoBar', 'commentDetail']); + +export default withFragments({ + root: gql` + fragment Comment_root on RootQuery { + __typename + ${pluginFragments.spreads('root')} + } + ${pluginFragments.definitions('root')} + `, + comment: gql` + fragment Comment_comment on Comment { + id + body + created_at + status + tags { + name + } + user { + id + name: username + } + action_summaries { + __typename + count + current_user { + id + } + } + ${pluginFragments.spreads('comment')} + } + ${pluginFragments.definitions('comment')} + `, +})(Comment); diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js new file mode 100644 index 000000000..12e05eed8 --- /dev/null +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -0,0 +1,113 @@ +import React from 'react'; +import {compose, gql, graphql} from 'react-apollo'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import isEqual from 'lodash/isEqual'; + +import {Spinner} from 'coral-ui'; +import {authActions, assetActions, pym} from 'coral-framework'; +import {getDefinitionName, separateDataAndRoot} from 'coral-framework/utils'; +import Embed from '../components/Embed'; +import {setCommentCountCache, viewAllComments} from '../actions/stream'; +import {setActiveTab} from '../actions/embed'; +import Stream from './Stream'; + +const {logout, checkLogin} = authActions; +const {fetchAssetSuccess} = assetActions; + +class EmbedContainer extends React.Component { + + componentDidMount() { + pym.sendMessage('childReady'); + this.props.checkLogin(); + } + + componentWillReceiveProps(nextProps) { + if(this.props.root.me && !nextProps.root.me) { + + // Refetch because on logout `excludeIgnored` becomes `false`. + // TODO: logout via mutation and obsolete this? + this.props.data.refetch(); + } + + const {fetchAssetSuccess} = this.props; + if(!isEqual(nextProps.root.asset, this.props.root.asset)) { + + // TODO: remove asset data from redux store. + fetchAssetSuccess(nextProps.root.asset); + + const {setCommentCountCache, commentCountCache} = this.props; + const {asset} = nextProps.root; + + if (commentCountCache === -1) { + setCommentCountCache(asset.commentCount); + } + } + } + + componentDidUpdate(prevProps) { + if(!isEqual(prevProps.root.comment, this.props.root.comment)) { + + // Scroll to a permalinked comment if one is in the URL once the page is done rendering. + setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0); + } + } + + render() { + if (!this.props.root.asset) { + return ; + } + return ; + } +} + +const EMBED_QUERY = gql` + query EmbedQuery($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) { + asset(id: $assetId, url: $assetUrl) { + totalCommentCount(excludeIgnored: $excludeIgnored) + } + me { + status + } + ...${getDefinitionName(Stream.fragments.root)} + } + ${Stream.fragments.root} +`; + +export const withQuery = graphql(EMBED_QUERY, { + options: ({auth, commentId, assetId, assetUrl}) => ({ + variables: { + assetId, + assetUrl, + commentId, + hasComment: commentId !== '', + excludeIgnored: Boolean(auth && auth.user && auth.user.id), + }, + }), + props: ({data}) => separateDataAndRoot(data), +}); + +const mapStateToProps = state => ({ + auth: state.auth.toJS(), + commentCountCache: state.stream.commentCountCache, + commentId: state.stream.commentId, + assetId: state.stream.assetId, + assetUrl: state.stream.assetUrl, + activeTab: state.embed.activeTab, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators({ + fetchAssetSuccess, + checkLogin, + setCommentCountCache, + viewAllComments, + logout, + setActiveTab, + }, dispatch); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withQuery, +)(EmbedContainer); + diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js new file mode 100644 index 000000000..4df7fc960 --- /dev/null +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -0,0 +1,262 @@ +import React from 'react'; +import {gql, compose} from 'react-apollo'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import uniqBy from 'lodash/uniqBy'; +import sortBy from 'lodash/sortBy'; +import isNil from 'lodash/isNil'; +import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream'; +import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; +import {notificationActions, authActions} from 'coral-framework'; +import {editName} from 'coral-framework/actions/user'; +import {setCommentCountCache, setActiveReplyBox} from '../actions/stream'; +import Stream from '../components/Stream'; +import Comment from './Comment'; +import withFragments from 'coral-framework/hocs/withFragments'; +import {getDefinitionName} from 'coral-framework/utils'; + +const {showSignInDialog} = authActions; +const {addNotification} = notificationActions; + +class StreamContainer extends React.Component { + getCounts = ({asset_id, limit, sort}) => { + return this.props.data.fetchMore({ + query: LOAD_COMMENT_COUNTS_QUERY, + variables: { + asset_id, + limit, + sort, + excludeIgnored: this.props.data.variables.excludeIgnored, + }, + updateQuery: (oldData, {fetchMoreResult:{asset}}) => { + return { + ...oldData, + asset: { + ...oldData.asset, + commentCount: asset.commentCount + } + }; + } + }); + }; + + // handle paginated requests for more Comments pertaining to the Asset + loadMore = ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => { + return this.props.data.fetchMore({ + query: LOAD_MORE_QUERY, + variables: { + limit, // how many comments are we returning + cursor, // the date of the first/last comment depending on the sort order + parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment + asset_id, // the id of the asset we're currently on + sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL + excludeIgnored: this.props.data.variables.excludeIgnored, + }, + updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => { + let updatedAsset; + + if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment + + let comment = {}; + if (oldData.comment && oldData.comment.parent) { + + // put comments (replies) onto the oldData.comment.parent object + // the initial comment permalinked was a reply + const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id'); + comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')}; + } else if (oldData.comment) { + + // put the comments (replies) directly onto oldData.comment + // the initial comment permalinked was a top-level comment + const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id'); + comment.replies = sortBy(uniqReplies, 'created_at'); + } + + updatedAsset = { + ...oldData, + comment: { + ...oldData.comment, + ...comment + } + }; + + } else if (parent_id) { // If loading more replies + + updatedAsset = { + ...oldData, + asset: { + ...oldData.asset, + comments: oldData.asset.comments.map(comment => { + + // since the dipslayed replies and the returned replies can overlap, + // pull out the unique ones. + const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id'); + + // since we just gave the returned replies precedence, they're now out of order. + // resort according to date. + return comment.id === parent_id + ? {...comment, replies: sortBy(uniqueReplies, 'created_at')} + : comment; + }) + } + }; + } else { // If loading more top-level comments + + updatedAsset = { + ...oldData, + asset: { + ...oldData.asset, + comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments] + : [...oldData.asset.comments, ...new_top_level_comments] + } + }; + } + + return updatedAsset; + } + }); + }; + + componentDidMount() { + this.props.data.refetch(); + this.countPoll = setInterval(() => { + const {asset} = this.props.root; + this.getCounts({ + asset_id: asset.id, + limit: asset.comments.length, + sort: 'REVERSE_CHRONOLOGICAL' + }); + }, NEW_COMMENT_COUNT_POLL_INTERVAL); + } + + componentWillUnmount() { + clearInterval(this.countPoll); + } + + render() { + return ; + } +} + +const LOAD_COMMENT_COUNTS_QUERY = gql` + query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) { + asset(id: $asset_id) { + id + commentCount + comments(sort: $sort, limit: $limit) { + id + replyCount + } + } + } +`; + +const LOAD_MORE_QUERY = gql` + query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { + new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { + ...${getDefinitionName(Comment.fragments.comment)} + replyCount(excludeIgnored: $excludeIgnored) + replies(limit: 3) { + ...${getDefinitionName(Comment.fragments.comment)} + } + } + } + ${Comment.fragments.comment} +`; + +const fragments = { + root: gql` + fragment Stream_root on RootQuery { + comment(id: $commentId) @include(if: $hasComment) { + ...${getDefinitionName(Comment.fragments.comment)} + replyCount(excludeIgnored: $excludeIgnored) + replies { + ...${getDefinitionName(Comment.fragments.comment)} + } + parent { + ...${getDefinitionName(Comment.fragments.comment)} + replyCount(excludeIgnored: $excludeIgnored) + replies { + ...${getDefinitionName(Comment.fragments.comment)} + } + } + } + asset(id: $assetId, url: $assetUrl) { + id + title + url + closedAt + created_at + settings { + moderation + infoBoxEnable + infoBoxContent + premodLinksEnable + questionBoxEnable + questionBoxContent + closeTimeout + closedMessage + charCountEnable + charCount + requireEmailConfirmation + } + lastComment { + id + } + commentCount(excludeIgnored: $excludeIgnored) + totalCommentCount(excludeIgnored: $excludeIgnored) + comments(limit: 10, excludeIgnored: $excludeIgnored) { + ...${getDefinitionName(Comment.fragments.comment)} + replyCount(excludeIgnored: $excludeIgnored) + replies(limit: 3, excludeIgnored: $excludeIgnored) { + ...${getDefinitionName(Comment.fragments.comment)} + } + } + } + myIgnoredUsers { + id, + username, + } + me { + status + } + ...${getDefinitionName(Comment.fragments.root)} + } + ${Comment.fragments.root} + ${Comment.fragments.comment} + `, +}; + +const mapStateToProps = state => ({ + auth: state.auth.toJS(), + commentCountCache: state.stream.commentCountCache, + activeReplyBox: state.stream.activeReplyBox, + + commentId: state.stream.commentId, + assetId: state.stream.assetId, + assetUrl: state.stream.assetUrl, + activeTab: state.embed.activeTab, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators({ + showSignInDialog, + addNotification, + setActiveReplyBox, + editName, + setCommentCountCache, + }, dispatch); + +export default compose( + withFragments(fragments), + connect(mapStateToProps, mapDispatchToProps), + postComment, + postFlag, + postLike, + postDontAgree, + addCommentTag, + removeCommentTag, + ignoreUser, + deleteAction, +)(StreamContainer); + diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index d7ee99e73..f3f6075b6 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -3,10 +3,13 @@ import {render} from 'react-dom'; import {ApolloProvider} from 'react-apollo'; import {client} from 'coral-framework/services/client'; -import localStore from 'coral-framework/services/store'; +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( diff --git a/client/coral-embed-stream/src/reducers/embed.js b/client/coral-embed-stream/src/reducers/embed.js new file mode 100644 index 000000000..e7fe5a533 --- /dev/null +++ b/client/coral-embed-stream/src/reducers/embed.js @@ -0,0 +1,17 @@ +import * as actions from '../constants/embed'; + +const initialState = { + activeTab: 'stream', +}; + +export default function stream(state = initialState, action) { + switch (action.type) { + case actions.SET_ACTIVE_TAB: + return { + ...state, + activeTab: action.tab, + }; + default: + return state; + } +} diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js new file mode 100644 index 000000000..a9049fc14 --- /dev/null +++ b/client/coral-embed-stream/src/reducers/index.js @@ -0,0 +1,7 @@ +import stream from './stream'; +import embed from './embed'; + +export default { + stream, + embed, +}; diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js new file mode 100644 index 000000000..59f068530 --- /dev/null +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -0,0 +1,45 @@ +import * as actions from '../constants/stream'; + +function getQueryVariable(variable) { + let query = window.location.search.substring(1); + let vars = query.split('&'); + for (let i = 0; i < vars.length; i++) { + let pair = vars[i].split('='); + if (decodeURIComponent(pair[0]) === variable) { + return decodeURIComponent(pair[1]); + } + } + + // If not found, return null. + return null; +} + +const initialState = { + activeReplyBox: '', + commentCountCache: -1, + assetId: getQueryVariable('asset_id'), + assetUrl: getQueryVariable('asset_url'), + commentId: getQueryVariable('comment_id'), +}; + +export default function stream(state = initialState, action) { + switch (action.type) { + case actions.SET_ACTIVE_REPLY_BOX: + return { + ...state, + activeReplyBox: action.id, + }; + case actions.SET_COMMENT_COUNT_CACHE: + return { + ...state, + commentCountCache: action.amount, + }; + case actions.VIEW_ALL_COMMENTS: + return { + ...state, + commentId: '', + }; + default: + return state; + } +} diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 722f4ca8e..950fa91bc 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -57,6 +57,10 @@ function configurePymParent(pymParent) { window.document.body.appendChild(snackbar); + // Workaround: IOS Safari ignores `width` but respects `min-width` value. + pymParent.el.firstChild.style.width = '1px'; + pymParent.el.firstChild.style.minWidth = '100%'; + // Resize parent iframe height when child height changes pymParent.onMessage('height', function(height) { if (height !== cachedHeight) { diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js index 50efa3dcb..6de4eaf9f 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-framework/actions/asset.js @@ -1,7 +1,6 @@ import * as actions from '../constants/asset'; import coralApi from '../helpers/response'; import {addNotification} from '../actions/notification'; -import {pym} from 'coral-framework'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; @@ -39,7 +38,6 @@ export const updateOpenStream = closedBody => (dispatch, getState) => { const openStream = () => ({type: actions.OPEN_COMMENTS}); const closeStream = () => ({type: actions.CLOSE_COMMENTS}); -export const updateCountCache = (id, count) => ({type: actions.UPDATE_COUNT_CACHE, id, count}); export const updateOpenStatus = status => dispatch => { if (status === 'open') { @@ -51,36 +49,3 @@ export const updateOpenStatus = status => dispatch => { } }; -function removeParam(key, sourceURL) { - let rtn = sourceURL.split('?')[0]; - let param; - let params_arr = []; - let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : ''; - if (queryString !== '') { - params_arr = queryString.split('&'); - for (let i = params_arr.length - 1; i >= 0; i -= 1) { - param = params_arr[i].split('=')[0]; - if (param === key) { - params_arr.splice(i, 1); - } - } - rtn = `${rtn}?${params_arr.join('&')}`; - } - return rtn; -} - -export const viewAllComments = () => { - - // remove the comment_id url param - const modifiedUrl = removeParam('comment_id', location.href); - try { - - // "window" here refers to the embedded iframe - window.history.replaceState({}, document.title, modifiedUrl); - - // also change the parent url - pym.sendMessage('coral-view-all-comments'); - } catch (e) { /* not sure if we're worried about old browsers */ } - - return {type: actions.VIEW_ALL_COMMENTS}; -}; diff --git a/client/coral-framework/constants/asset.js b/client/coral-framework/constants/asset.js index 5547c5284..c26dd099d 100644 --- a/client/coral-framework/constants/asset.js +++ b/client/coral-framework/constants/asset.js @@ -8,6 +8,4 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE'; export const OPEN_COMMENTS = 'OPEN_COMMENTS'; export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; -export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE'; -export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS'; diff --git a/client/coral-framework/constants/comments.js b/client/coral-framework/constants/comments.js deleted file mode 100644 index 28db9cdf9..000000000 --- a/client/coral-framework/constants/comments.js +++ /dev/null @@ -1,2 +0,0 @@ -export const ADDTL_COMMENTS_ON_LOAD_MORE = 10; -export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000; diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js index 4a3abb48e..eb0b5386a 100644 --- a/client/coral-framework/graphql/mutations/index.js +++ b/client/coral-framework/graphql/mutations/index.js @@ -9,10 +9,6 @@ import REMOVE_COMMENT_TAG from './removeCommentTag.graphql'; import IGNORE_USER from './ignoreUser.graphql'; import STOP_IGNORING_USER from './stopIgnoringUser.graphql'; -import MY_IGNORED_USERS from '../queries/myIgnoredUsers.graphql'; -import STREAM_QUERY from '../queries/streamQuery.graphql'; -import {variablesForStreamQuery} from '../queries'; - import commentView from '../fragments/commentView.graphql'; export const postComment = graphql(POST_COMMENT, { @@ -45,7 +41,7 @@ export const postComment = graphql(POST_COMMENT, { } }, updateQueries: { - AssetQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => { + EmbedQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => { if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') { return oldData; @@ -155,6 +151,7 @@ export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, { }}), }); +// TODO: don't rely on refetching. export const ignoreUser = graphql(IGNORE_USER, { props: ({mutate}) => ({ ignoreUser: ({id}) => { @@ -162,15 +159,16 @@ export const ignoreUser = graphql(IGNORE_USER, { variables: { id, }, - refetchQueries: [{ - query: MY_IGNORED_USERS, - }] + refetchQueries: [ + 'EmbedQuery', 'myIgnoredUsers', + ] }); }}), }); +// TODO: don't rely on refetching. export const stopIgnoringUser = graphql(STOP_IGNORING_USER, { - props: ({mutate, ownProps}) => { + props: ({mutate}) => { return { stopIgnoringUser: ({id}) => { return mutate({ @@ -178,13 +176,7 @@ export const stopIgnoringUser = graphql(STOP_IGNORING_USER, { id, }, refetchQueries: [ - { - query: MY_IGNORED_USERS, - }, - { - query: STREAM_QUERY, - variables: variablesForStreamQuery(ownProps), - } + 'EmbedQuery', 'myIgnoredUsers', ] }); } diff --git a/client/coral-framework/graphql/queries/commentQuery.graphql b/client/coral-framework/graphql/queries/commentQuery.graphql deleted file mode 100644 index 83f92b8f5..000000000 --- a/client/coral-framework/graphql/queries/commentQuery.graphql +++ /dev/null @@ -1,13 +0,0 @@ -#import "../fragments/commentView.graphql" - -query commentQuery($id: ID!) { - comment(id: $id) { - ...commentView - parent { - ...commentView - replies { - ...commentView - } - } - } -} diff --git a/client/coral-framework/graphql/queries/getCounts.graphql b/client/coral-framework/graphql/queries/getCounts.graphql deleted file mode 100644 index ff09a0498..000000000 --- a/client/coral-framework/graphql/queries/getCounts.graphql +++ /dev/null @@ -1,10 +0,0 @@ -query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) { - asset(id: $asset_id) { - id - commentCount - comments(sort: $sort, limit: $limit) { - id - replyCount - } - } -} diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index 5dbea5822..d76614a68 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -1,153 +1,6 @@ import {graphql} from 'react-apollo'; -import STREAM_QUERY from './streamQuery.graphql'; -import LOAD_MORE from './loadMore.graphql'; -import GET_COUNTS from './getCounts.graphql'; import MY_COMMENT_HISTORY from './myCommentHistory.graphql'; import MY_IGNORED_USERS from './myIgnoredUsers.graphql'; -import uniqBy from 'lodash/uniqBy'; -import sortBy from 'lodash/sortBy'; -import isNil from 'lodash/isNil'; - -function getQueryVariable(variable) { - let query = window.location.search.substring(1); - let vars = query.split('&'); - for (let i = 0; i < vars.length; i++) { - let pair = vars[i].split('='); - if (decodeURIComponent(pair[0]) === variable) { - return decodeURIComponent(pair[1]); - } - } - - // If not found, return null. - return null; -} - -// get the counts of the top-level comments -export const getCounts = (data) => ({asset_id, limit, sort}) => { - return data.fetchMore({ - query: GET_COUNTS, - variables: { - asset_id, - limit, - sort, - excludeIgnored: data.variables.excludeIgnored, - }, - updateQuery: (oldData, {fetchMoreResult:{asset}}) => { - return { - ...oldData, - asset: { - ...oldData.asset, - commentCount: asset.commentCount - } - }; - } - }); -}; - -// handle paginated requests for more Comments pertaining to the Asset -export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => { - return data.fetchMore({ - query: LOAD_MORE, - variables: { - limit, // how many comments are we returning - cursor, // the date of the first/last comment depending on the sort order - parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment - asset_id, // the id of the asset we're currently on - sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL - excludeIgnored: data.variables.excludeIgnored, - }, - updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => { - let updatedAsset; - - if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment - - let comment = {}; - if (oldData.comment && oldData.comment.parent) { - - // put comments (replies) onto the oldData.comment.parent object - // the initial comment permalinked was a reply - const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id'); - comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')}; - } else if (oldData.comment) { - - // put the comments (replies) directly onto oldData.comment - // the initial comment permalinked was a top-level comment - const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id'); - comment.replies = sortBy(uniqReplies, 'created_at'); - } - - updatedAsset = { - ...oldData, - comment: { - ...oldData.comment, - ...comment - } - }; - - } else if (parent_id) { // If loading more replies - - updatedAsset = { - ...oldData, - asset: { - ...oldData.asset, - comments: oldData.asset.comments.map(comment => { - - // since the dipslayed replies and the returned replies can overlap, - // pull out the unique ones. - const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id'); - - // since we just gave the returned replies precedence, they're now out of order. - // resort according to date. - return comment.id === parent_id - ? {...comment, replies: sortBy(uniqueReplies, 'created_at')} - : comment; - }) - } - }; - } else { // If loading more top-level comments - - updatedAsset = { - ...oldData, - asset: { - ...oldData.asset, - comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments] - : [...oldData.asset.comments, ...new_top_level_comments] - } - }; - } - - return updatedAsset; - } - }); -}; - -export const variablesForStreamQuery = ({auth}) => { - - // where the query string is from the embeded iframe url - let comment_id = getQueryVariable('comment_id'); - let has_comment = comment_id != null; - return { - asset_id: getQueryVariable('asset_id'), - asset_url: getQueryVariable('asset_url'), - comment_id: has_comment ? comment_id : 'no-comment', - has_comment, - excludeIgnored: Boolean(auth && auth.user && auth.user.id), - }; -}; - -// load the comment stream. -export const queryStream = graphql(STREAM_QUERY, { - options: (props) => { - return { - variables: variablesForStreamQuery(props) - }; - }, - props: ({data}) => ({ - data, - loadMore: loadMore(data), - getCounts: getCounts(data), - }) -}); export const myCommentHistory = graphql(MY_COMMENT_HISTORY, {}); diff --git a/client/coral-framework/graphql/queries/loadMore.graphql b/client/coral-framework/graphql/queries/loadMore.graphql deleted file mode 100644 index b18f4d84e..000000000 --- a/client/coral-framework/graphql/queries/loadMore.graphql +++ /dev/null @@ -1,11 +0,0 @@ -#import "../fragments/commentView.graphql" - -query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { - new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { - ...commentView - replyCount(excludeIgnored: $excludeIgnored) - replies(limit: 3) { - ...commentView - } - } -} diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql deleted file mode 100644 index c9a7690fc..000000000 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ /dev/null @@ -1,53 +0,0 @@ -#import "../fragments/commentView.graphql" - -query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!, $excludeIgnored: Boolean) { - # the comment here is for loading one comment and it's children, probably after following a permalink - # $has_comment is derived from the comment_id query param in the iframe url, - # which is in turn pulled from the host page url - comment(id: $comment_id) @include(if: $has_comment) { - ...commentView - replyCount(excludeIgnored: $excludeIgnored) - replies { - ...commentView - } - parent { - ...commentView - replyCount(excludeIgnored: $excludeIgnored) - replies { - ...commentView - } - } - } - asset(id: $asset_id, url: $asset_url) { - id - title - url - closedAt - created_at - settings { - moderation - infoBoxEnable - infoBoxContent - premodLinksEnable - questionBoxEnable - questionBoxContent - closeTimeout - closedMessage - charCountEnable - charCount - requireEmailConfirmation - } - lastComment { - id - } - commentCount(excludeIgnored: $excludeIgnored) - totalCommentCount(excludeIgnored: $excludeIgnored) - comments(limit: 10, excludeIgnored: $excludeIgnored) { - ...commentView - replyCount(excludeIgnored: $excludeIgnored) - replies(limit: 3, excludeIgnored: $excludeIgnored) { - ...commentView - } - } - } -} diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index e0d345db4..df50fdcb7 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -1,7 +1,11 @@ import React from 'react'; import merge from 'lodash/merge'; import flatten from 'lodash/flatten'; +import flattenDeep from 'lodash/flattenDeep'; +import uniq from 'lodash/uniq'; import plugins from 'pluginsConfig'; +import {gql} from 'react-apollo'; +import {getDefinitionName} from 'coral-framework/utils'; export const pluginReducers = merge( ...plugins @@ -19,3 +23,53 @@ export function getSlotElements(slot, props = {}) { return components .map((component, i) => React.createElement(component, {...props, key: i})); } + +function getComponentFragments(components) { + return components + .map(c => c.fragments) + .filter(fragments => fragments) + .reduce((res, fragments) => { + Object.keys(fragments).forEach(key => { + if (!(key in res)) { + res[key] = {spreads: '', definitions: ''}; + } + res[key].spreads += `...${getDefinitionName(fragments[key])}\n`; + res[key].definitions = gql`${res[key].definitions}${fragments[key]}`; + }); + return res; + }, {}); +} + +/** + * Returns an object that can be used to compose fragments or queries. + * + * Example: + * const pluginFragments = getSlotsFragments(['commentInfoBar', 'commentDetail']); + * const rootFragment = gql` + * fragment Comment_root on RootQuery { + + ${pluginFragments.spreads('root')} + * } + * ${pluginFragments.definitions('root')} + * `; + */ +export function getSlotsFragments(slots) { + if (!Array.isArray(slots)) { + slots = [slots]; + } + const components = uniq(flattenDeep(slots.map(slot => { + return plugins + .filter(o => o.module.slots[slot]) + .map(o => o.module.slots[slot]); + }))); + + const fragments = getComponentFragments(components); + return { + spreads(key) { + return fragments[key] && fragments[key].spreads; + }, + definitions(key) { + return fragments[key] && fragments[key].definitions; + }, + }; +} + diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js new file mode 100644 index 000000000..a2686d94b --- /dev/null +++ b/client/coral-framework/hocs/withFragments.js @@ -0,0 +1,18 @@ +import React from 'react'; + +// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38. + +function getDisplayName(WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} + +export default fragments => WrappedComponent => { + class WithFragments extends React.Component { + render() { + return ; + } + } + WithFragments.fragments = fragments; + WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`; + return WithFragments; +}; diff --git a/client/coral-framework/reducers/asset.js b/client/coral-framework/reducers/asset.js index 067edfc5b..f9d0a55e3 100644 --- a/client/coral-framework/reducers/asset.js +++ b/client/coral-framework/reducers/asset.js @@ -19,9 +19,6 @@ export default function asset (state = initialState, action) { case actions.UPDATE_ASSET_SETTINGS_SUCCESS: return state .setIn(['settings'], action.settings); - case actions.UPDATE_COUNT_CACHE: - return state - .setIn(['countCache', action.id], action.count); default: return state; } diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index d7090f4b5..f6ace1fd3 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -24,14 +24,22 @@ if (window.devToolsExtension) { middlewares.push(window.devToolsExtension()); } -const store = createStore( - combineReducers({ - ...mainReducer, - apollo: client.reducer() - }), +let storeReducers = { + ...mainReducer, + apollo: client.reducer() +}; + +export const store = createStore( + combineReducers(storeReducers), {}, compose(...middlewares) ); export default store; + +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 4e9c29db7..598fb59ab 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -29,3 +29,35 @@ export const getMyActionSummary = (type, comment) => { export const getActionSummary = (type, comment) => { return comment.action_summaries.filter(a => a.__typename === type); }; + +/** + * Get name of first (or $pos-th) definition + */ +export function getDefinitionName(doc, pos = 0) { + return doc.definitions[pos].name.value; +} + +/** + * Separate apollo `data` props into `data` and `root`. + * `data` will contain props like `loading`, `fetchMore`... + * while `root` contains the actual query data. + */ +export function separateDataAndRoot( + { + fetchMore, + loading, + networkStatus, + refetch, + startPolling, + stopPolling, + subscribeToMore, + updateQuery, + variables, + ...root, + }) { + return { + data: {fetchMore, loading, networkStatus, refetch, startPolling, + stopPolling, subscribeToMore, updateQuery, variables}, + root, + }; +} diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 261c95414..6dd2d486f 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -24,14 +24,14 @@ class CommentBox extends Component { postComment = () => { const { + commentPostedHandler, + postItem, + setCommentCountCache, + commentCountCache, isReply, assetId, parentId, - postItem, - countCache, addNotification, - updateCountCache, - commentPostedHandler } = this.props; let comment = { @@ -41,7 +41,7 @@ class CommentBox extends Component { ...this.props.commentBox }; - !isReply && updateCountCache(assetId, countCache + 1); + !isReply && setCommentCountCache(commentCountCache + 1); // Execute preSubmit Hooks this.state.hooks.preSubmit.forEach(hook => hook()); @@ -55,18 +55,20 @@ class CommentBox extends Component { if (postedComment.status === 'REJECTED') { addNotification('error', lang.t('comment-post-banned-word')); - !isReply && updateCountCache(assetId, countCache); + !isReply && setCommentCountCache(commentCountCache); } else if (postedComment.status === 'PREMOD') { addNotification('success', lang.t('comment-post-notif-premod')); - !isReply && updateCountCache(assetId, countCache); + !isReply && setCommentCountCache(commentCountCache); } if (commentPostedHandler) { commentPostedHandler(); } }) - .catch((err) => console.error(err)); - + .catch((err) => { + console.error(err); + !isReply && setCommentCountCache(commentCountCache); + }); this.setState({body: ''}); } @@ -196,7 +198,6 @@ CommentBox.propTypes = { authorId: PropTypes.string.isRequired, isReply: PropTypes.bool.isRequired, canPost: PropTypes.bool, - currentUser: PropTypes.object }; const mapStateToProps = ({commentBox}) => ({commentBox}); diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 553504462..39e615725 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -12,7 +12,6 @@ import NotLoggedIn from '../components/NotLoggedIn'; 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'; @@ -35,14 +34,14 @@ class ProfileContainer extends Component { } render() { - const {loggedIn, asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props; + const {asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props; const {me} = this.props.data; if (data.loading) { return ; } - if (!loggedIn || !me) { + if (!me) { return ; } @@ -51,7 +50,7 @@ class ProfileContainer extends Component { return (
    -

    {this.props.userData.username}

    +

    {this.props.user.username}

    { emailAddress ?

    { emailAddress }

    : null diff --git a/client/coral-sign-in/components/FakeComment.js b/client/coral-sign-in/components/FakeComment.js index f5926852c..37630383d 100644 --- a/client/coral-sign-in/components/FakeComment.js +++ b/client/coral-sign-in/components/FakeComment.js @@ -1,5 +1,5 @@ import React from 'react'; -import styles from 'coral-embed-stream/src/Comment.css'; +import styles from 'coral-embed-stream/src/components/Comment.css'; import AuthorName from 'coral-plugin-author-name/AuthorName'; import Content from 'coral-plugin-commentcontent/CommentContent'; diff --git a/client/coral-ui/components/Button.css b/client/coral-ui/components/Button.css index 14ed64aa2..219b70a65 100644 --- a/client/coral-ui/components/Button.css +++ b/client/coral-ui/components/Button.css @@ -149,7 +149,7 @@ box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09); width: 128px; -&:hover { + &:hover { color: white; background-color: #D03235; box-shadow: none; diff --git a/client/coral-ui/components/Icon.js b/client/coral-ui/components/Icon.js index 11432c753..b7c49a799 100644 --- a/client/coral-ui/components/Icon.js +++ b/client/coral-ui/components/Icon.js @@ -1,8 +1,12 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Icon as IconMDL} from 'react-mdl'; const Icon = ({className = '', name}) => ( ); +Icon.propTypes = { + name: PropTypes.string.isRequired +}; + export default Icon; diff --git a/package.json b/package.json index 6c725a852..e2bd683ca 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ "parse-duration": "^0.1.1", "passport": "^0.3.2", "passport-local": "^1.0.0", - "react-apollo": "^1.0.0", + "prop-types": "^15.5.8", + "react-apollo": "^1.1.0", "react-recaptcha": "^2.2.6", "redis": "^2.7.1", "resolve": "^1.3.2", @@ -100,7 +101,7 @@ "uuid": "^2.0.3" }, "devDependencies": { - "apollo-client": "^1.0.0", + "apollo-client": "^1.0.4", "autoprefixer": "^6.5.2", "babel-cli": "^6.24.0", "babel-core": "^6.24.0", diff --git a/plugins/coral-plugin-respect/client/components/RespectButton.js b/plugins/coral-plugin-respect/client/components/RespectButton.js index c1e0c1b47..faeda1991 100644 --- a/plugins/coral-plugin-respect/client/components/RespectButton.js +++ b/plugins/coral-plugin-respect/client/components/RespectButton.js @@ -12,8 +12,8 @@ const lang = new I18n(translations); class RespectButton extends Component { handleClick = () => { - const {postRespect, showSignInDialog, deleteAction, commentId} = this.props; - const {me, comment} = this.props.data; + const {postRespect, showSignInDialog, deleteAction} = this.props; + const {root: {me}, comment} = this.props; const myRespectActionSummary = getMyActionSummary('RespectActionSummary', comment); @@ -29,17 +29,17 @@ class RespectButton extends Component { } if (myRespectActionSummary) { - deleteAction(myRespectActionSummary.current_user.id); + deleteAction(myRespectActionSummary.current_user.id, comment.id); } else { postRespect({ - item_id: commentId, + item_id: comment.id, item_type: 'COMMENTS' }); } } render() { - const {comment} = this.props.data; + const {comment} = this.props; if (!comment) { return null; diff --git a/plugins/coral-plugin-respect/client/containers/RespectButton.js b/plugins/coral-plugin-respect/client/containers/RespectButton.js index bcf0e342d..38be8a978 100644 --- a/plugins/coral-plugin-respect/client/containers/RespectButton.js +++ b/plugins/coral-plugin-respect/client/containers/RespectButton.js @@ -2,37 +2,25 @@ import {compose, gql, graphql} from 'react-apollo'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import get from 'lodash/get'; - +import withFragments from 'coral-framework/hocs/withFragments'; import {showSignInDialog} from 'coral-framework/actions/auth'; import RespectButton from '../components/RespectButton'; -// TODO: use `update` instead of `updateQueries` for optimistic mutations. -// 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) { - id - action_summaries { - ... on RespectActionSummary { - count - current_user { - id - } +const COMMENT_FRAGMENT = gql` + fragment RespectButton_updateFragment on Comment { + action_summaries { + ... on RespectActionSummary { + count + current_user { + id } } } - me { - status - } } `; -const withQuery = graphql(RESPECT_QUERY); - const withDeleteAction = graphql(gql` mutation deleteAction($id: ID!) { deleteAction(id:$id) { @@ -43,7 +31,7 @@ const withDeleteAction = graphql(gql` } `, { props: ({mutate}) => ({ - deleteAction: (id) => { + deleteAction: (id, commentId) => { return mutate({ variables: {id}, optimisticResponse: { @@ -52,27 +40,26 @@ const withDeleteAction = graphql(gql` errors: null, } }, - updateQueries: { - RespectQuery: (prev) => { - 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: action_summaries.map( - (a, i) => i !== idx ? a : ({ - ...a, - count: a.count - 1, - current_user: null, - })), - } - }; - return next; - }, + update: (proxy) => { + const fragmentId = `Comment_${commentId}`; + + // Read the data from our cache for this query. + const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); + + // Check whether we respected this comment. + const idx = data.action_summaries.findIndex(isRespectAction); + if (idx < 0 || get(data.action_summaries[idx], 'current_user.id') !== id) { + return; + } + + data.action_summaries[idx] = { + ...data.action_summaries[idx], + count: data.action_summaries[idx].count - 1, + current_user: null, + }; + + // Write our data back to the cache. + proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); }, }); }, @@ -105,46 +92,39 @@ const withPostRespect = graphql(gql` }, } }, - updateQueries: { - RespectQuery: (prev, {mutationResult, queryVariables}) => { - if (queryVariables.commentId !== respect.item_id) { - return prev; - } + update: (proxy, mutationResult) => { + const fragmentId = `Comment_${respect.item_id}`; - let action_summaries = prev.comment.action_summaries; - let idx = action_summaries.findIndex(isRespectAction); + // Read the data from our cache for this query. + const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - // Check whether we already respected this comment. - if (idx >= 0 && action_summaries[idx].current_user) { - return prev; - } + // Add our comment from the mutation to the end. + let idx = data.action_summaries.findIndex(isRespectAction); - if (idx < 0) { + // Check whether we already respected this comment. + if (idx >= 0 && data.action_summaries[idx].current_user) { + return; + } - // 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; - } + if (idx < 0) { - const respectAction = mutationResult.data.createRespect.respect; - const next = { - ...prev, - comment: { - ...prev.comment, - action_summaries: action_summaries.map( - (a, i) => i !== idx ? a : ({ - ...a, - count: a.count + 1, - current_user: respectAction, - })), - } - }; - return next; - }, + // Add initial action when it doesn't exist. + data.action_summaries.push({ + __typename: 'RespectActionSummary', + count: 0, + current_user: null, + }); + idx = data.action_summaries.length - 1; + } + + data.action_summaries[idx] = { + ...data.action_summaries[idx], + count: data.action_summaries[idx].count + 1, + current_user: mutationResult.data.createRespect.respect, + }; + + // Write our data back to the cache. + proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); }, }); }, @@ -155,10 +135,29 @@ const mapDispatchToProps = dispatch => bindActionCreators({showSignInDialog}, dispatch); const enhance = compose( + withFragments({ + root: gql` + fragment RespectButton_root on RootQuery { + me { + status + } + } + `, + comment: gql` + fragment RespectButton_comment on Comment { + action_summaries { + ... on RespectActionSummary { + count + current_user { + id + } + } + } + }`, + }), connect(null, mapDispatchToProps), withDeleteAction, withPostRespect, - withQuery, ); export default enhance(RespectButton); diff --git a/yarn.lock b/yarn.lock index 378e5a6c0..34025cfd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -179,11 +179,11 @@ anymatch@^1.3.0: arrify "^1.0.0" micromatch "^2.1.5" -apollo-client@^1.0.0, apollo-client@^1.0.0-rc.9: - version "1.0.2" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.2.tgz#4355bd49d53a1489bc91d9f56d5b3d0ffe33fb3c" +apollo-client@^1.0.2, apollo-client@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.4.tgz#af75db8cdd27e08a835ddfb39807849e178540f9" dependencies: - graphql "^0.9.1" + graphql "^0.9.3" graphql-anywhere "^3.0.1" graphql-tag "^2.0.0" redux "^3.4.0" @@ -2948,6 +2948,18 @@ fbjs@^0.8.1, fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +fbjs@^0.8.9: + version "0.8.12" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + fd-slicer@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" @@ -3513,10 +3525,6 @@ graphql-tag@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.2.3.tgz#74c62443fbf3e693647426d7359f7e3e6ce7dace" -graphql-tag@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.2.tgz#7abb3a8fd9f3415d07163314ed237061c785b759" - graphql-tag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.0.0.tgz#f3efe3b4d64f33bfe8479ae06a461c9d72f2a6fe" @@ -3543,9 +3551,9 @@ graphql@^0.8.2: dependencies: iterall "1.0.2" -graphql@^0.9.1: - version "0.9.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.9.2.tgz#2cb5c635de13f790a77c5879649cb401b1589386" +graphql@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.9.3.tgz#71fc0fa331bffb9c20678485861cfb370803118e" dependencies: iterall "1.0.3" @@ -6366,6 +6374,12 @@ promise@^7.0.1, promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.5.8: + version "15.5.8" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" + dependencies: + fbjs "^0.8.9" + protocols@^1.1.0, protocols@^1.4.0: version "1.4.3" resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.3.tgz#635b1c0785f0b389e8a012df1b1afffda9608b76" @@ -6613,13 +6627,13 @@ react-addons-test-utils@^15.4.2: fbjs "^0.8.4" object-assign "^4.1.0" -react-apollo@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.0.0.tgz#7fcc14adcc7aa4ca4d9e04ddedf50b8fb74daa91" +react-apollo@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.1.0.tgz#0c5027da72420919b62083e4c473cf406959892c" dependencies: - apollo-client "^1.0.0-rc.9" + apollo-client "^1.0.2" graphql-anywhere "^3.0.0" - graphql-tag "^1.3.1" + graphql-tag "^2.0.0" hoist-non-react-statics "^1.2.0" invariant "^2.2.1" lodash.flatten "^4.2.0"