Merge branch 'master' into subscriptions

This commit is contained in:
David Erwin
2017-04-26 16:55:32 -04:00
committed by GitHub
60 changed files with 9698 additions and 900 deletions
+3
View File
@@ -39,6 +39,9 @@ const routes = (
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path='all' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
<Route path='premod' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
@@ -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 (
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
className={`${typeName} ${styles.actionButton} ${active ? styles[`${typeName}__active`] : ''}`}
cStyle={typeName}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
>{menuActionsMap[type].text}</Button>
);
};
ActionButton.propTypes = {
status: PropTypes.string
};
export default ActionButton;
@@ -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;
}
@@ -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 {
<ModerationHeader asset={asset} />
<ModerationMenu
asset={asset}
allCount={data.allCount}
premodCount={data.premodCount}
rejectedCount={data.rejectedCount}
flaggedCount={data.flaggedCount}
@@ -21,7 +21,6 @@ const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loa
key={i}
index={i}
comment={comment}
commentType={activeTab}
selected={i === selectedIndex}
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
@@ -23,6 +23,12 @@ const Comment = ({actions = [], comment, ...props}) => {
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 (
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
@@ -36,7 +42,7 @@ const Comment = ({actions = [], comment, ...props}) => {
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
<CommentType type={props.commentType} />
<CommentType type={commentType} />
</div>
{comment.user.status === 'banned' ?
<span className={styles.banned}>
@@ -64,6 +70,7 @@ const Comment = ({actions = [], comment, ...props}) => {
<ActionButton key={i}
type={action}
user={comment.user}
status={comment.status}
acceptComment={() => props.acceptComment({commentId: comment.id})}
rejectComment={() => props.rejectComment({commentId: comment.id})}
/>
@@ -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
};
@@ -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 (
<div className="mdl-tabs">
@@ -27,22 +23,28 @@ const ModerationMenu = (
<div className={styles.tabBarPadding} />
<div>
<Link
to={premodPath}
to={getPath('all')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
<Icon name='question_answer' className={styles.tabIcon} /> {lang.t('modqueue.all')} <CommentCount count={allCount} />
</Link>
<Link
to={flagPath}
to={getPath('premod')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
<Icon name='access_time' className={styles.tabIcon} /> {lang.t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
<Link
to={rejectPath}
to={getPath('flagged')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
<Icon name='flag' className={styles.tabIcon} /> {lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={getPath('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='close' className={styles.tabIcon} /> {lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link>
</div>
<SelectField
@@ -59,6 +61,7 @@ const ModerationMenu = (
};
ModerationMenu.propTypes = {
allCount: PropTypes.number.isRequired,
premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired,
flaggedCount: PropTypes.number.isRequired,
@@ -418,3 +418,8 @@ span {
.loadMore:hover {
background-color: #4399FF;
}
.tabIcon {
position: relative;
top: 7px;
}
@@ -36,6 +36,9 @@ export const getMetrics = graphql(METRICS, {
export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
let statuses;
switch(tab) {
case 'all':
statuses = null;
break;
case 'premod':
statuses = ['PREMOD'];
break;
@@ -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
+1 -1
View File
@@ -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 = (
-348
View File
@@ -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 <Spinner />;
}
// 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 = <UserBox user={user} logout={() => 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 (
<div style={expandForLogin}>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.totalCommentCount}/></Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
highlightedComment &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={() => {
this.props.viewAllComments();
this.props.data.refetch();
}}>{lang.t('showAllComments')}</Button>
}
<TabContent show={activeTab === 0}>
{ loggedIn ? userBox : null }
{
openStream
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
/>
<RestrictedContent restricted={banned} restrictedComp={
<SuspendedAccount
canEditName={user && user.canEditName}
editName={this.props.editName}
/>
}>
{
user
? <CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
updateCountCache={this.props.updateCountCache}
countCache={countCache[asset.id]}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={this.props.auth.user}
authorId={user.id}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount} />
: null
}
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <Button id='coralSignInButton' onClick={this.props.showSignInDialog} full>Sign in to comment</Button>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
{
highlightedComment
? <HighlightedComment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
addNotification={this.props.addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
highlighted={comment.id}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
commentIsIgnored={commentIsIgnored}
key={highlightedComment.id}
reactKey={highlightedComment.id}
comment={highlightedComment} />
: <div>
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<div className="embed__stream">
<Stream
open={openStream}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
ignoreUser={this.props.ignoreUser}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments}
maxCharCount={asset.settings.charCount}
charCountEnable={asset.settings.charCountEnable}
ignoredUsers={this.props.userData.ignoredUsers} />
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={hasOlderComments}
loadMore={this.props.loadMore} />
</div>
}
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
loggedIn={loggedIn}
userData={this.props.userData}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
{ loggedIn ? userBox : null }
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</div>
</div>
);
}
}
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);
-103
View File
@@ -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 (
<div id='stream'>
{
comments.map(comment =>
commentIsIgnored(comment)
? <IgnoredCommentTombstone
key={comment.id}
/>
: <Comment
disableReply={!open}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
maxCharCount={maxCharCount}
charCountEnable={charCountEnable}
pluginProps={pluginProps}
/>
)
}
</div>
);
}
}
export default Stream;
@@ -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());
}
};
@@ -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};
};
@@ -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 {
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Slot fill="commentInfoBar" comment={comment} commentId={comment.id} inline/>
<Slot
fill="commentInfoBar"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
{ (currentUser && (comment.user.id !== currentUser.id))
? <span className={styles.topRightMenu}>
<TopRightMenu
@@ -221,7 +227,14 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
<Slot fill="commentDetail" comment={comment} commentId={comment.id} inline/>
<Slot
fill="commentDetail"
data={this.props.data}
root={this.props.root}
comment={comment}
commentId={comment.id}
inline
/>
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
@@ -263,6 +276,8 @@ class Comment extends React.Component {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
@@ -0,0 +1,85 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Button} from 'coral-ui';
import Stream from '../containers/Stream';
import Count from 'coral-plugin-comment-count/CommentCount';
import UserBox from 'coral-sign-in/components/UserBox';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
export default class Embed extends React.Component {
changeTab = (tab) => {
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 = <UserBox user={user} logout={logout} changeTab={this.changeTab}/>;
return (
<div>
<div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={totalCommentCount}/></Tab>
<Tab>{lang.t('myProfile')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
commentId &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={viewAllComments}
>
{lang.t('showAllComments')}
</Button>
}
<TabContent show={activeTab === 'stream'}>
{ loggedIn ? userBox : null }
<Stream data={this.props.data} root={this.props.root} />
</TabContent>
<TabContent show={activeTab === 'profile'}>
<ProfileContainer />
</TabContent>
<TabContent show={activeTab === 'config'}>
<RestrictedContent restricted={!loggedIn}>
{ loggedIn ? userBox : null }
<ConfigureStreamContainer />
</RestrictedContent>
</TabContent>
</div>
</div>
);
}
}
Embed.propTypes = {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired,
};
@@ -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);
@@ -3,23 +3,23 @@ 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,
cursor: firstCommentDate,
assetId,
sort: 'CHRONOLOGICAL'
}, true);
};
const NewCount = (props) => {
const newComments = props.commentCount - props.countCache;
const newComments = props.commentCount - props.commentCountCache;
return <div className='coral-new-comments coral-load-more'>
{
props.countCache && newComments > 0 ?
props.commentCountCache && newComments > 0 ?
<button onClick={onLoadMoreClick(props)}>
{newComments === 1
? lang.t('newCount', newComments, lang.t('comment'))
@@ -32,7 +32,7 @@ const NewCount = (props) => {
NewCount.propTypes = {
commentCount: PropTypes.number.isRequired,
countCache: PropTypes.number,
commentCountCache: PropTypes.number,
loadMore: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired,
firstCommentDate: PropTypes.string.isRequired
@@ -0,0 +1,207 @@
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import Comment from '../containers/Comment';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import SuspendedAccount from 'coral-framework/components/SuspendedAccount';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
import {ModerationLink} from 'coral-plugin-moderation';
class Stream extends React.Component {
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
} else {
this.props.setActiveReplyBox(reactKey);
}
}
render () {
const {
root: {asset, asset: {comments}, comment, myIgnoredUsers},
postItem,
addNotification,
postFlag,
postLike,
postDontAgree,
loadMore,
deleteAction,
showSignInDialog,
addCommentTag,
removeCommentTag,
pluginProps,
ignoreUser,
auth: {loggedIn, isAdmin, user},
commentCountCache,
editName,
} = this.props;
const open = asset.closedAt === null;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent ? comment.parent : comment;
const banned = user && user.status === 'BANNED';
const hasOlderComments = !!(
asset &&
asset.lastComment &&
asset.lastComment.id !== asset.comments[asset.comments.length - 1].id
);
// 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 commentIsIgnored = (comment) => myIgnoredUsers && myIgnoredUsers.includes(comment.user.id);
return (
<div id='stream'>
{
open
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<QuestionBox
content={asset.settings.questionBoxContent}
enable={asset.settings.questionBoxEnable}
/>
<RestrictedContent restricted={banned} restrictedComp={
<SuspendedAccount
canEditName={user && user.canEditName}
editName={editName}
/>
}>
{
user
? <CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
setCommentCountCache={this.props.setCommentCountCache}
commentCountCache={commentCountCache}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
authorId={user.id}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount} />
: null
}
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <Button id='coralSignInButton' onClick={this.props.showSignInDialog} full>Sign in to comment</Button>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
{
highlightedComment
? <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
highlighted={comment.id}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={commentIsIgnored}
reactKey={highlightedComment.id}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>
: <div>
<NewCount
commentCount={asset.commentCount}
commentCountCache={commentCountCache}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
setCommentCountCache={this.props.setCommentCountCache}
/>
<div className="embed__stream">
{
comments.map(comment =>
commentIsIgnored(comment)
? <IgnoredCommentTombstone
key={comment.id}
/>
: <Comment
data={this.props.data}
root={this.props.root}
disableReply={!open}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={user}
postLike={postLike}
postFlag={postFlag}
postDontAgree={postDontAgree}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadMore}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment}
pluginProps={pluginProps}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>
)
}
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={hasOlderComments}
loadMore={this.props.loadMore} />
</div>
}
</div>
);
}
}
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;
@@ -0,0 +1 @@
export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB';
@@ -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';
@@ -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);
@@ -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 <Spinner />;
}
return <Embed {...this.props} />;
}
}
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);
@@ -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 <Stream {...this.props} loadMore={this.loadMore}/>;
}
}
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);
+4 -1
View File
@@ -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(
@@ -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;
}
}
@@ -0,0 +1,7 @@
import stream from './stream';
import embed from './embed';
export default {
stream,
embed,
};
@@ -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;
}
}
+4
View File
@@ -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) {
-35
View File
@@ -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};
};
@@ -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';
@@ -1,2 +0,0 @@
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
@@ -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',
]
});
}
@@ -1,13 +0,0 @@
#import "../fragments/commentView.graphql"
query commentQuery($id: ID!) {
comment(id: $id) {
...commentView
parent {
...commentView
replies {
...commentView
}
}
}
}
@@ -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
}
}
}
@@ -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, {});
@@ -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
}
}
}
@@ -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
}
}
}
}
+54
View File
@@ -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;
},
};
}
@@ -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 <WrappedComponent {...this.props} />;
}
}
WithFragments.fragments = fragments;
WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`;
return WithFragments;
};
-3
View File
@@ -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;
}
+13 -5
View File
@@ -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;
+32
View File
@@ -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,
};
}
+11 -10
View File
@@ -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});
@@ -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 <Spinner/>;
}
if (!loggedIn || !me) {
if (!me) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
@@ -51,7 +50,7 @@ class ProfileContainer extends Component {
return (
<div>
<h2>{this.props.userData.username}</h2>
<h2>{this.props.user.username}</h2>
{ emailAddress
? <p>{ emailAddress }</p>
: null
@@ -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';
+1 -1
View File
@@ -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;
+5 -1
View File
@@ -1,8 +1,12 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Icon as IconMDL} from 'react-mdl';
const Icon = ({className = '', name}) => (
<IconMDL className={className} name={name} />
);
Icon.propTypes = {
name: PropTypes.string.isRequired
};
export default Icon;
+3 -2
View File
@@ -93,7 +93,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",
"uuid": "^3.0.1",
@@ -102,7 +103,7 @@
"semver": "^5.3.0"
},
"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",
@@ -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;
@@ -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);
+8524 -14
View File
File diff suppressed because it is too large Load Diff