mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
merge conficts
This commit is contained in:
@@ -34,3 +34,8 @@ export const storySearchChange = (value) => ({
|
||||
export const clearState = () => ({
|
||||
type: actions.MODERATION_CLEAR_STATE
|
||||
});
|
||||
|
||||
export const selectCommentId = (id) => ({
|
||||
type: actions.MODERATION_SELECT_COMMENT,
|
||||
id,
|
||||
});
|
||||
|
||||
@@ -21,10 +21,11 @@ class CommentDetails extends Component {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
this.props.clearHeightCache && this.props.clearHeightCache();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {data, root, comment} = this.props;
|
||||
const {data, root, comment, clearHeightCache} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
const queryData = {
|
||||
root,
|
||||
@@ -44,12 +45,14 @@ class CommentDetails extends Component {
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
{showDetail && <Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>}
|
||||
</div>
|
||||
@@ -61,6 +64,7 @@ CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CommentDetails;
|
||||
|
||||
@@ -6,3 +6,4 @@ export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
|
||||
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
|
||||
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
|
||||
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
|
||||
export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
|
||||
|
||||
@@ -144,7 +144,7 @@ UserDetailContainer.propTypes = {
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
...CoralAdmin_UserDetail_CommentConnection
|
||||
}
|
||||
}
|
||||
${commentConnectionFragment}
|
||||
|
||||
@@ -7,6 +7,7 @@ const initialState = {
|
||||
storySearchString: '',
|
||||
shortcutsNoteVisible: 'show',
|
||||
sortOrder: 'DESC',
|
||||
selectedCommentId: '',
|
||||
};
|
||||
|
||||
export default function moderation (state = initialState, action) {
|
||||
@@ -51,6 +52,11 @@ export default function moderation (state = initialState, action) {
|
||||
...state,
|
||||
sortOrder: action.order,
|
||||
};
|
||||
case actions.MODERATION_SELECT_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
selectedCommentId: action.id,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* AutoLoadMore with call `loadMore` the moment it is rendered and shows a Spinner.
|
||||
*/
|
||||
class AutoLoadMore extends React.Component {
|
||||
componentDidMount() {
|
||||
if(!this.props.loading) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Spinner />;
|
||||
}
|
||||
}
|
||||
|
||||
AutoLoadMore.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AutoLoadMore;
|
||||
@@ -10,7 +10,9 @@
|
||||
position: relative;
|
||||
transition: all 200ms;
|
||||
padding: 10px 0;
|
||||
margin-top: 13px;
|
||||
min-height: 0;
|
||||
outline: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
@@ -25,6 +27,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dangling {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
@@ -17,21 +17,36 @@ import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
ref = null;
|
||||
|
||||
handleRef = (ref) => (this.ref = ref);
|
||||
|
||||
handleFocusOrClick = () => {
|
||||
if (!this.props.selected) {
|
||||
this.props.selectComment();
|
||||
}
|
||||
};
|
||||
|
||||
viewUserDetail = () => {
|
||||
const {viewUserDetail, comment} = this.props;
|
||||
return viewUserDetail(comment.user.id);
|
||||
};
|
||||
|
||||
approve = () => (this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({commentId: this.props.comment.id})
|
||||
);
|
||||
approve = () =>
|
||||
this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({commentId: this.props.comment.id});
|
||||
|
||||
reject = () => (this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({commentId: this.props.comment.id})
|
||||
);
|
||||
reject = () =>
|
||||
this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({commentId: this.props.comment.id});
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
if (!prev.selected && this.props.selected) {
|
||||
this.ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -42,6 +57,8 @@ class Comment extends React.Component {
|
||||
root,
|
||||
root: {settings},
|
||||
currentAsset,
|
||||
clearHeightCache,
|
||||
dangling,
|
||||
} = this.props;
|
||||
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
@@ -50,34 +67,48 @@ class Comment extends React.Component {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected}, 'talk-admin-moderate-comment')}
|
||||
className={cn(
|
||||
className,
|
||||
'mdl-card',
|
||||
selectionStateCSS,
|
||||
styles.root,
|
||||
{[styles.selected]: selected, [styles.dangling]: dangling},
|
||||
'talk-admin-moderate-comment'
|
||||
)}
|
||||
id={`comment_${comment.id}`}
|
||||
onClick={this.handleFocusOrClick}
|
||||
ref={this.handleRef}
|
||||
onFocus={this.handleFocusOrClick}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
|
||||
<span
|
||||
className={cn(styles.username, 'talk-admin-moderate-comment-username')}
|
||||
onClick={this.viewUserDetail}>
|
||||
className={cn(
|
||||
styles.username,
|
||||
'talk-admin-moderate-comment-username'
|
||||
)}
|
||||
onClick={this.viewUserDetail}
|
||||
>
|
||||
{comment.user.username}
|
||||
</span>
|
||||
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
<CommentLabels
|
||||
comment={comment}
|
||||
/>
|
||||
<CommentLabels comment={comment} />
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
@@ -86,8 +117,11 @@ class Comment extends React.Component {
|
||||
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!currentAsset &&
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
|
||||
{!currentAsset && (
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.itemBody}>
|
||||
@@ -96,8 +130,7 @@ class Comment extends React.Component {
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
body={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
/>{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
@@ -109,6 +142,7 @@ class Comment extends React.Component {
|
||||
<Slot
|
||||
fill="adminCommentContent"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<div className={styles.sideActions}>
|
||||
@@ -130,6 +164,7 @@ class Comment extends React.Component {
|
||||
<Slot
|
||||
fill="adminSideActions"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
@@ -140,6 +175,7 @@ class Comment extends React.Component {
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
clearHeightCache={clearHeightCache}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -149,10 +185,14 @@ class Comment extends React.Component {
|
||||
Comment.propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
selectComment: PropTypes.func,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
dangling: PropTypes.bool,
|
||||
currentAsset: PropTypes.object,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
@@ -162,12 +202,12 @@ Comment.propTypes = {
|
||||
created_at: PropTypes.string.isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
status: PropTypes.string
|
||||
status: PropTypes.string,
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
data: PropTypes.object.isRequired,
|
||||
|
||||
@@ -12,15 +12,9 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import ViewOptions from './ViewOptions';
|
||||
|
||||
class Moderation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const comments = this.getComments(props);
|
||||
|
||||
this.state = {
|
||||
selectedCommentId: comments[0] ? comments[0].id : null,
|
||||
};
|
||||
|
||||
}
|
||||
state = {
|
||||
isLoadingMore: false,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const {toggleModal, singleView} = this.props;
|
||||
@@ -30,17 +24,16 @@ class Moderation extends Component {
|
||||
key('esc', () => toggleModal(false));
|
||||
key('ctrl+f', () => this.openSearch());
|
||||
key('t', () => this.nextQueue());
|
||||
key('j', () => this.select(true));
|
||||
key('k', () => this.select(false));
|
||||
key('f', () => this.moderate(false));
|
||||
key('d', () => this.moderate(true));
|
||||
this.getMenuItems()
|
||||
.forEach((menuItem, idx) => key(`${idx + 1}`, () => this.selectQueue(menuItem)));
|
||||
this.getMenuItems().forEach((menuItem, idx) =>
|
||||
key(`${idx + 1}`, () => this.selectQueue(menuItem))
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.toggleModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
nextQueue = () => {
|
||||
const activeTab = this.props.activeTab;
|
||||
@@ -48,39 +41,45 @@ class Moderation extends Component {
|
||||
const menuItems = this.getMenuItems();
|
||||
|
||||
const activeTabIndex = menuItems.findIndex((item) => item === activeTab);
|
||||
const nextQueueIndex = (activeTabIndex === menuItems.length - 1) ? 0 : activeTabIndex + 1;
|
||||
const nextQueueIndex =
|
||||
activeTabIndex === menuItems.length - 1 ? 0 : activeTabIndex + 1;
|
||||
|
||||
this.selectQueue(menuItems[nextQueueIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
selectQueue = (key) => {
|
||||
const assetId = this.props.data.variables.asset_id;
|
||||
this.props.router.push(this.props.getModPath(key, assetId));
|
||||
}
|
||||
};
|
||||
|
||||
getMenuItems = () => Object.keys(this.props.queueConfig);
|
||||
|
||||
closeSearch = () => {
|
||||
const {toggleStorySearch} = this.props;
|
||||
toggleStorySearch(false);
|
||||
}
|
||||
};
|
||||
|
||||
openSearch = () => {
|
||||
this.props.toggleStorySearch(true);
|
||||
}
|
||||
};
|
||||
|
||||
getActiveTabCount = (props = this.props) => {
|
||||
return props.root[`${props.activeTab}Count`];
|
||||
}
|
||||
};
|
||||
|
||||
moderate = (accept) => {
|
||||
const {acceptComment, rejectComment} = this.props;
|
||||
const {selectedCommentId} = this.state;
|
||||
const {
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
moderation: {selectedCommentId},
|
||||
} = this.props;
|
||||
|
||||
// Accept or reject only if there's a selected comment
|
||||
if(selectedCommentId != null){
|
||||
if (selectedCommentId != null) {
|
||||
const comments = this.getComments();
|
||||
const commentIdx = comments.findIndex((comment) => comment.id === selectedCommentId);
|
||||
const commentIdx = comments.findIndex(
|
||||
(comment) => comment.id === selectedCommentId
|
||||
);
|
||||
const comment = comments[commentIdx];
|
||||
|
||||
if (accept) {
|
||||
@@ -89,86 +88,27 @@ class Moderation extends Component {
|
||||
comment.status !== 'REJECTED' && rejectComment({commentId: comment.id});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getComments = (props = this.props) => {
|
||||
const {root, activeTab} = props;
|
||||
return root[activeTab].nodes;
|
||||
}
|
||||
|
||||
scrollTo = (toId, smooth = true) =>
|
||||
document.querySelector(`#comment_${toId}`).scrollIntoView(smooth ? {behavior: 'smooth'} : {});
|
||||
|
||||
select = async (next, props = this.props, selectedCommentId = this.state.selectedCommentId) => {
|
||||
const comments = this.getComments(props);
|
||||
|
||||
// No comments to be selected.
|
||||
if (comments.length === 0){
|
||||
return;
|
||||
}
|
||||
|
||||
// Find current index if we have a selected comment.
|
||||
const index = selectedCommentId
|
||||
? comments.findIndex((comment) => comment.id === selectedCommentId)
|
||||
: null;
|
||||
|
||||
if (next) {
|
||||
|
||||
// Grab first one if we don't have a selected comment yet.
|
||||
if (!selectedCommentId) {
|
||||
this.setState({selectedCommentId: comments[0].id}, () => this.scrollTo(comments[0].id));
|
||||
return;
|
||||
}
|
||||
|
||||
// Select next one when we still have more comments left.
|
||||
if (index < comments.length - 1) {
|
||||
this.setState({selectedCommentId: comments[index + 1].id}, () => this.scrollTo(comments[index + 1].id));
|
||||
return;
|
||||
} else {
|
||||
|
||||
// We hit the end of the list, load more comments if we have.
|
||||
if (comments.length < this.getActiveTabCount()) {
|
||||
const res = await this.loadMore();
|
||||
|
||||
// If `loadMore` was already in progress, res would be false.
|
||||
if (res) {
|
||||
|
||||
// Select next comment after loading has completed.
|
||||
this.select(true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
// We have no selected comment, so just skip it.
|
||||
if (!selectedCommentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we still have previous comments take the one before.
|
||||
if (index > 0) {
|
||||
this.setState({selectedCommentId: comments[index - 1].id}, () => this.scrollTo(comments[index - 1].id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadMore = async () => {
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
if (!this.state.isLoadingMore) {
|
||||
this.setState({isLoadingMore: true});
|
||||
try {
|
||||
const result = await this.props.loadMore(this.props.activeTab);
|
||||
this.isLoadingMore = false;
|
||||
this.setState({isLoadingMore: false});
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
this.isLoadingMore = false;
|
||||
} catch (e) {
|
||||
this.setState({isLoadingMore: false});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('s');
|
||||
@@ -176,58 +116,23 @@ class Moderation extends Component {
|
||||
key.unbind('esc');
|
||||
key.unbind('ctrl+f');
|
||||
key.unbind('t');
|
||||
key.unbind('j');
|
||||
key.unbind('k');
|
||||
key.unbind('f');
|
||||
key.unbind('d');
|
||||
this.getMenuItems()
|
||||
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
|
||||
this.getMenuItems().forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
if (this.props.activeTab !== nextProps.activeTab) {
|
||||
|
||||
// Reset selection when changing tabs.
|
||||
this.select(true, nextProps, null);
|
||||
} else {
|
||||
|
||||
// Detect if comment has left the queue and find next or prev selected comment to set it
|
||||
// as the new selectedCommentId.
|
||||
const prevComments = this.getComments(this.props);
|
||||
const nextComments = this.getComments(nextProps);
|
||||
if (nextComments.length < prevComments.length) {
|
||||
|
||||
// Comments have changed, now check if our selected comment has left the queue.
|
||||
if (
|
||||
this.state.selectedCommentId &&
|
||||
!nextComments.some((comment) => comment.id === this.state.selectedCommentId)
|
||||
) {
|
||||
|
||||
// Determine a comment to select.
|
||||
const prevIndex = prevComments.findIndex((comment) => comment.id === this.state.selectedCommentId);
|
||||
if (prevIndex !== prevComments.length - 1) {
|
||||
this.setState({selectedCommentId: prevComments[prevIndex + 1].id});
|
||||
} else if(prevIndex > 0) {
|
||||
this.setState({selectedCommentId: prevComments[prevIndex - 1].id});
|
||||
} else {
|
||||
this.setState({selectedCommentId: null});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
||||
// Scroll to comment when changing from single wiew to normal view.
|
||||
if (prevProps.moderation.singleView !== this.props.moderation.singleView && this.state.selectedCommentId) {
|
||||
this.scrollTo(this.state.selectedCommentId, false);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {root, data, moderation, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
data,
|
||||
moderation,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
getModPath,
|
||||
queueConfig,
|
||||
handleCommentChange,
|
||||
...props
|
||||
} = this.props;
|
||||
const {asset} = root;
|
||||
const assetId = asset && asset.id;
|
||||
|
||||
@@ -238,7 +143,7 @@ class Moderation extends Component {
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
icon: queueConfig[queue].icon,
|
||||
count: root[`${queue}Count`]
|
||||
count: root[`${queue}Count`],
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -255,7 +160,9 @@ class Moderation extends Component {
|
||||
items={menuItems}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
<div className={cn(styles.container, 'talk-admin-moderation-container')}>
|
||||
<div
|
||||
className={cn(styles.container, 'talk-admin-moderation-container')}
|
||||
>
|
||||
<ViewOptions
|
||||
selectSort={this.props.setSortOrder}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
@@ -266,15 +173,20 @@ class Moderation extends Component {
|
||||
root={this.props.root}
|
||||
currentAsset={asset}
|
||||
comments={comments.nodes}
|
||||
hasNextPage={comments.hasNextPage}
|
||||
activeTab={activeTab}
|
||||
singleView={moderation.singleView}
|
||||
selectedCommentId={this.state.selectedCommentId}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
loadMore={this.loadMore}
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
cleanUpQueue={props.cleanUpQueue}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
hideShortcutsNote={props.hideShortcutsNote}
|
||||
@@ -295,7 +207,7 @@ class Moderation extends Component {
|
||||
queryData={{root, asset}}
|
||||
activeTab={activeTab}
|
||||
handleCommentChange={handleCommentChange}
|
||||
fill='adminModeration'
|
||||
fill="adminModeration"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -305,12 +217,15 @@ class Moderation extends Component {
|
||||
Moderation.propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
toggleModal: PropTypes.func.isRequired,
|
||||
selectedCommentId: PropTypes.string,
|
||||
toggleStorySearch: PropTypes.func.isRequired,
|
||||
getModPath: PropTypes.func.isRequired,
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
storySearchChange: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
queueConfig: PropTypes.object.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
setSortOrder: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -2,29 +2,15 @@
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
min-height: 650px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.commentLeave {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.commentLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.commentEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,26 +4,34 @@ import PropTypes from 'prop-types';
|
||||
import Comment from '../containers/Comment';
|
||||
import styles from './ModerationQueue.css';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
import AutoLoadMore from './AutoLoadMore';
|
||||
import ViewMore from './ViewMore';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
import {
|
||||
WindowScroller,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
import throttle from 'lodash/throttle';
|
||||
import key from 'keymaster';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second comment of
|
||||
// the current comment list. The cursors are used to dertermine which
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
// the current comment The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted. Additionally the new view based on the new
|
||||
// cursors are also returned.
|
||||
function resetCursors(state, props) {
|
||||
let idCursors = [];
|
||||
if (props.comments && props.comments.length) {
|
||||
const idCursors = [props.comments[0].id];
|
||||
idCursors.push(props.comments[0].id);
|
||||
if (props.comments[1]) {
|
||||
idCursors.push(props.comments[1].id);
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
return {idCursors: []};
|
||||
const view = getVisibleComments(props.comments, idCursors[0]);
|
||||
return {idCursors, view};
|
||||
}
|
||||
|
||||
// invalidateCursor is called whenever a comment is removed which is referenced
|
||||
@@ -40,91 +48,322 @@ function invalidateCursor(invalidated, state, props) {
|
||||
idCursors.push(nextInLine.id);
|
||||
}
|
||||
}
|
||||
return {idCursors};
|
||||
return idCursors;
|
||||
}
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which comes after the `idCursor`.
|
||||
function getVisibleComments(comments, idCursor) {
|
||||
if (!comments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
comments.forEach((comment) => {
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
if (pastCursor) {
|
||||
view.push(comment);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
// Current keymapper to use for the CellMeasurer Cache.
|
||||
let keyMapper = null;
|
||||
|
||||
// CellMeasurerCache is used to measure the size of the elements
|
||||
// of the virtual list. We use a global one with a keyMapper that
|
||||
// should resolve to a comment id, which is then used to cache the height.
|
||||
const cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: 250,
|
||||
keyMapper: (index) => keyMapper(index),
|
||||
});
|
||||
|
||||
class ModerationQueue extends React.Component {
|
||||
isLoadingMore = false;
|
||||
listRef = null;
|
||||
callbackCaches = {
|
||||
clearHeightCache: {},
|
||||
selectCommentId: {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...resetCursors(this.state, props),
|
||||
};
|
||||
|
||||
// Set keyMapper to map to comment ids.
|
||||
keyMapper = (index) => {
|
||||
const view = this.state.view;
|
||||
if (index < view.length) {
|
||||
return view[index].id;
|
||||
} else if (index === view.length) {
|
||||
return 'loadMore';
|
||||
}
|
||||
throw new Error(`unknown index ${index}`);
|
||||
};
|
||||
|
||||
// Select first comment.
|
||||
if (this.state.view.length) {
|
||||
props.selectCommentId(this.state.view[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
const {comments, commentCount} = this.props;
|
||||
componentDidMount() {
|
||||
key('j', () => this.selectDown());
|
||||
key('k', () => this.selectUp());
|
||||
|
||||
// if the user just moderated the last (visible) comment
|
||||
// AND there are more comments available on the server,
|
||||
// go ahead and load more comments
|
||||
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
// TODO: Workaround for issue https://github.com/bvaughn/react-virtualized/issues/866
|
||||
this.reflowList();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('j');
|
||||
key.unbind('k');
|
||||
|
||||
// When switching queues, clean it up first.
|
||||
// Removes dangling comments and reduce overly large
|
||||
// lists and restore chronological order.
|
||||
this.props.cleanUpQueue(this.props.activeTab);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const {comments: prevComments} = this.props;
|
||||
const {comments: nextComments} = next;
|
||||
|
||||
if (!prevComments && nextComments) {
|
||||
this.setState(resetCursors);
|
||||
// New comments where added and our cursor list is incomplete.
|
||||
if (
|
||||
this.state.idCursors.length < 2 &&
|
||||
nextComments.length > this.state.idCursors.length
|
||||
) {
|
||||
this.setState(resetCursors(this.state, next));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
nextComments.length < prevComments.length
|
||||
) {
|
||||
let idCursors = this.state.idCursors;
|
||||
|
||||
// Comments have been removed.
|
||||
if (
|
||||
prevComments &&
|
||||
nextComments &&
|
||||
nextComments.length < prevComments.length
|
||||
) {
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) {
|
||||
this.setState(invalidateCursor(0, this.state, next));
|
||||
if (
|
||||
this.state.idCursors[0] &&
|
||||
!hasComment(nextComments, this.state.idCursors[0])
|
||||
) {
|
||||
idCursors = invalidateCursor(0, this.state, next);
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) {
|
||||
this.setState(invalidateCursor(1, this.state, next));
|
||||
if (
|
||||
this.state.idCursors[1] &&
|
||||
!hasComment(nextComments, this.state.idCursors[1])
|
||||
) {
|
||||
idCursors = invalidateCursor(1, this.state, next);
|
||||
}
|
||||
|
||||
// Selected comment was removed, determine and set next selected comment.
|
||||
if (
|
||||
this.props.selectedCommentId &&
|
||||
!hasComment(nextComments, this.props.selectedCommentId)
|
||||
) {
|
||||
const view = this.state.view;
|
||||
let nextSelectedCommentId = null;
|
||||
|
||||
// Determine a comment to select.
|
||||
const prevIndex = view.findIndex(
|
||||
(comment) => comment.id === this.props.selectedCommentId
|
||||
);
|
||||
if (prevIndex !== view.length - 1) {
|
||||
nextSelectedCommentId = view[prevIndex + 1].id;
|
||||
} else if (prevIndex > 0) {
|
||||
nextSelectedCommentId = view[prevIndex - 1].id;
|
||||
}
|
||||
this.props.selectCommentId(nextSelectedCommentId);
|
||||
}
|
||||
}
|
||||
|
||||
// Comments changed.
|
||||
if (prevComments !== nextComments) {
|
||||
const nextView = getVisibleComments(nextComments, idCursors[0]);
|
||||
this.setState({idCursors, view: nextView});
|
||||
|
||||
// TODO: removing or adding a comment from the list seems to render incorrect, is this a bug?
|
||||
// Find first changed comment and perform a reflow.
|
||||
const index = this.state.view.findIndex(
|
||||
(comment, i) => !nextView[i] || nextView[i].id !== comment.id
|
||||
);
|
||||
this.reflowList(index);
|
||||
}
|
||||
}
|
||||
|
||||
viewNewComments = () => {
|
||||
this.setState(resetCursors);
|
||||
componentDidUpdate(prev) {
|
||||
const {commentCount, selectedCommentId} = this.props;
|
||||
|
||||
// If the user just moderated the last (visible) comment
|
||||
// AND there are more comments available on the server,
|
||||
// go ahead and load more comments
|
||||
if (
|
||||
prev.comments.length > 0 &&
|
||||
this.getCommentCountWithoutDagling() === 0 &&
|
||||
commentCount > 0
|
||||
) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
|
||||
// Scroll to selected comment.
|
||||
if (prev.selectedCommentId !== selectedCommentId && this.listRef) {
|
||||
const view = this.state.view;
|
||||
const index = view.findIndex(({id}) => id === selectedCommentId);
|
||||
|
||||
this.listRef.scrollToRow(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns comment counts without dangling comments.
|
||||
getCommentCountWithoutDagling(props = this.props) {
|
||||
return props.comments.filter((comment) =>
|
||||
props.commentBelongToQueue(props.activeTab, comment)
|
||||
).length;
|
||||
}
|
||||
|
||||
async selectDown() {
|
||||
const view = this.state.view;
|
||||
const index = view.findIndex(({id}) => id === this.props.selectedCommentId);
|
||||
if (
|
||||
index === view.length - 1 &&
|
||||
this.getCommentCountWithoutDagling() !== this.props.commentCount
|
||||
) {
|
||||
await this.props.loadMore();
|
||||
this.selectDown();
|
||||
return;
|
||||
}
|
||||
if (index < view.length - 1) {
|
||||
this.props.selectCommentId(view[index + 1].id);
|
||||
}
|
||||
}
|
||||
|
||||
selectUp() {
|
||||
const view = this.state.view;
|
||||
const index = view.findIndex(({id}) => id === this.props.selectedCommentId);
|
||||
|
||||
if (index === 0 && view.length < this.props.comments.length) {
|
||||
this.viewNewComments(() => this.selectUp());
|
||||
return;
|
||||
}
|
||||
if (index > 0) {
|
||||
this.props.selectCommentId(view[index - 1].id);
|
||||
}
|
||||
}
|
||||
|
||||
handleListRef = (list) => {
|
||||
this.listRef = list;
|
||||
};
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
const {comments} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
viewNewComments = (callback) => {
|
||||
this.setState(resetCursors, () => {
|
||||
this.reflowList();
|
||||
callback && callback();
|
||||
});
|
||||
};
|
||||
|
||||
if (!comments) {
|
||||
return [];
|
||||
reflowList = throttle((index) => {
|
||||
if (index >= 0) {
|
||||
cache.clear(index);
|
||||
this.listRef && this.listRef.recomputeRowHeights(index);
|
||||
} else {
|
||||
cache.clearAll();
|
||||
this.listRef && this.listRef.recomputeRowHeights();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
rowRenderer = ({
|
||||
index, // Index of row within collection
|
||||
parent,
|
||||
style, // Style object to be applied to row (to position it)
|
||||
}) => {
|
||||
const view = this.state.view;
|
||||
const rowCount = view.length + 1;
|
||||
|
||||
let child = null;
|
||||
let key = null;
|
||||
|
||||
// Last element of list is our AutoLoadMore component and contains an
|
||||
// id indicating that this is the last element in list.
|
||||
if (index === rowCount - 1) {
|
||||
key = 'end-of-comment-list';
|
||||
child = (
|
||||
<div style={style} id={'end-of-comment-list'}>
|
||||
{this.props.hasNextPage && (
|
||||
<AutoLoadMore
|
||||
loadMore={this.props.loadMore}
|
||||
loading={this.props.isLoadingMore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const comment = view[index];
|
||||
|
||||
// Use callback cache so not to change the identity of these arrow functions.
|
||||
// Otherwise shallow compare will fail to optimize.
|
||||
if (!this.callbackCaches.clearHeightCache[index]) {
|
||||
this.callbackCaches.clearHeightCache[index] = () =>
|
||||
this.reflowList(index);
|
||||
}
|
||||
if (!this.callbackCaches.selectCommentId[comment.id]) {
|
||||
this.callbackCaches.selectCommentId[comment.id] = () =>
|
||||
this.props.selectCommentId(comment.id);
|
||||
}
|
||||
|
||||
key = comment.id;
|
||||
child = (
|
||||
<div style={style}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
comment={comment}
|
||||
dangling={
|
||||
!this.props.commentBelongToQueue(this.props.activeTab, comment)
|
||||
}
|
||||
selected={comment.id === this.props.selectedCommentId}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
acceptComment={this.props.acceptComment}
|
||||
rejectComment={this.props.rejectComment}
|
||||
currentAsset={this.props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
clearHeightCache={this.callbackCaches.clearHeightCache[index]}
|
||||
selectComment={this.callbackCaches.selectCommentId[comment.id]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
comments.forEach((comment) => {
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
if (pastCursor) {
|
||||
view.push(comment);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
return (
|
||||
<CellMeasurer
|
||||
cache={cache}
|
||||
columnIndex={0}
|
||||
key={key}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
{child}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
comments,
|
||||
selectedCommentId,
|
||||
commentCount,
|
||||
singleView,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
@@ -137,7 +376,9 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
if (singleView) {
|
||||
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
|
||||
const index = comments.findIndex(
|
||||
(comment) => comment.id === selectedCommentId
|
||||
);
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -157,67 +398,55 @@ class ModerationQueue extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const view = this.getVisibleComments();
|
||||
const view = this.state.view;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<ViewMore
|
||||
viewMore={this.viewNewComments}
|
||||
viewMore={() => this.viewNewComments()}
|
||||
count={comments.length - view.length}
|
||||
/>
|
||||
<CSSTransitionGroup
|
||||
key={activeTab}
|
||||
component={'ul'}
|
||||
className={styles.list}
|
||||
transitionName={{
|
||||
enter: styles.commentEnter,
|
||||
enterActive: styles.commentEnterActive,
|
||||
leave: styles.commentLeave,
|
||||
leaveActive: styles.commentLeaveActive,
|
||||
}}
|
||||
transitionEnter={true}
|
||||
transitionLeave={true}
|
||||
transitionEnterTimeout={1000}
|
||||
transitionLeaveTimeout={1000}
|
||||
>
|
||||
{
|
||||
view
|
||||
.map((comment) => {
|
||||
return <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
selected={comment.id === selectedCommentId}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
|
||||
<LoadMore
|
||||
loadMore={this.props.loadMore}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
/>
|
||||
<WindowScroller onResize={this.reflowList}>
|
||||
{({height, isScrolling, onChildScroll, scrollTop}) => (
|
||||
<List
|
||||
ref={this.handleListRef}
|
||||
autoHeight
|
||||
className={styles.list}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
height={height}
|
||||
width={1280}
|
||||
scrollTop={scrollTop}
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
rowCount={view.length + 1}
|
||||
deferredMeasurementCache={cache}
|
||||
rowRenderer={this.rowRenderer}
|
||||
rowHeight={cache.rowHeight}
|
||||
/>
|
||||
)}
|
||||
</WindowScroller>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
selectCommentId: PropTypes.func.isRequired,
|
||||
selectedCommentId: PropTypes.string,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
selectedCommentId: PropTypes.string,
|
||||
singleView: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
hasNextPage: PropTypes.bool,
|
||||
comments: PropTypes.array,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
background-color: #2376D8;
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
margin: 0;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
.viewMore:hover {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
overflow: visible;
|
||||
height: 144px;
|
||||
min-height: auto;
|
||||
z-index: 4;
|
||||
margin-top: 16px;
|
||||
z-index: 10;
|
||||
|
||||
@media (--tablet) {
|
||||
width: 650px;
|
||||
|
||||
@@ -11,7 +11,12 @@ import NotFoundAsset from '../components/NotFoundAsset';
|
||||
import {isPremod, getModPath} from '../../../utils';
|
||||
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {handleCommentChange} from '../graphql';
|
||||
import {
|
||||
handleCommentChange,
|
||||
commentBelongToQueue,
|
||||
cleanUpQueue,
|
||||
} from '../graphql';
|
||||
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
import {
|
||||
toggleModal,
|
||||
@@ -20,7 +25,8 @@ import {
|
||||
toggleStorySearch,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState
|
||||
clearState,
|
||||
selectCommentId,
|
||||
} from 'actions/moderation';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
@@ -63,13 +69,15 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
|
||||
const {root: {asset, settings}} = this.props;
|
||||
const id = getAssetId(this.props);
|
||||
const tab = getTab(this.props);
|
||||
|
||||
// Grab premod from asset or from settings if it's defined.
|
||||
const setting = id && asset && asset.settings ? asset.settings.moderation : settings.moderation;
|
||||
const setting =
|
||||
id && asset && asset.settings
|
||||
? asset.settings.moderation
|
||||
: settings.moderation;
|
||||
|
||||
const queue = isPremod(setting) ? 'premod' : 'new';
|
||||
const activeTab = tab ? tab : queue;
|
||||
@@ -82,60 +90,101 @@ class ModerationContainer extends Component {
|
||||
{
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentAdded: comment}}}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentAccepted: comment}}}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_accepted',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body)
|
||||
);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentRejected: comment}}}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_rejected',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body)
|
||||
);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentReset: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_reset', user.username, prepareNotificationText(comment.body));
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentReset: comment}}}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_reset',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body)
|
||||
);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentEdited: comment}}}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentFlagged: comment}}}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMoreThrottled(param));
|
||||
this.subscriptions = parameters.map((param) =>
|
||||
this.props.data.subscribeToMoreThrottled(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
@@ -158,24 +207,42 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
// Resubscribe when we change between assets.
|
||||
if(this.props.data.variables.asset_id !== nextProps.data.variables.asset_id) {
|
||||
if (
|
||||
this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
|
||||
) {
|
||||
this.resubscribe(nextProps.data.variables);
|
||||
}
|
||||
}
|
||||
|
||||
cleanUpQueue = (queue) => {
|
||||
if (!this.props.data.loading) {
|
||||
this.props.data.updateQuery((query) => {
|
||||
return cleanUpQueue(
|
||||
query,
|
||||
queue,
|
||||
this.props.moderation.sortOrder,
|
||||
this.props.queueConfig
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
acceptComment = ({commentId}) => {
|
||||
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
|
||||
}
|
||||
};
|
||||
|
||||
rejectComment = ({commentId}) => {
|
||||
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
};
|
||||
|
||||
commentBelongToQueue = (queue, comment) => {
|
||||
return commentBelongToQueue(queue, comment, this.props.queueConfig);
|
||||
};
|
||||
|
||||
loadMore = (tab) => {
|
||||
const variables = {
|
||||
limit: 10,
|
||||
limit: 20,
|
||||
cursor: this.props.root[tab].endCursor,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
asset_id: this.props.data.variables.asset_id,
|
||||
@@ -186,20 +253,19 @@ class ModerationContainer extends Component {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables,
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
|
||||
return update(prev, {
|
||||
[tab]: {
|
||||
nodes: {$push: comments.nodes},
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
startCursor: {$set: comments.startCursor},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {root, root: {asset, settings}, data} = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
@@ -209,20 +275,19 @@ class ModerationContainer extends Component {
|
||||
|
||||
if (assetId) {
|
||||
if (asset === null) {
|
||||
|
||||
// Not found.
|
||||
return <NotFoundAsset assetId={assetId} />;
|
||||
}
|
||||
}
|
||||
|
||||
if(data.loading) {
|
||||
|
||||
if (data.loading) {
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
|
||||
isPremod(settings.moderation);
|
||||
const premodEnabled = assetId
|
||||
? isPremod(asset.settings.moderation)
|
||||
: isPremod(settings.moderation);
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
|
||||
@@ -234,16 +299,21 @@ class ModerationContainer extends Component {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
return <Moderation
|
||||
{...this.props}
|
||||
getModPath={getModPath}
|
||||
loadMore={this.loadMore}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
activeTab={this.activeTab}
|
||||
queueConfig={currentQueueConfig}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
/>;
|
||||
return (
|
||||
<Moderation
|
||||
{...this.props}
|
||||
getModPath={getModPath}
|
||||
loadMore={this.loadMore}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
activeTab={this.activeTab}
|
||||
queueConfig={currentQueueConfig}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
selectedCommentId={this.props.selectedCommentId}
|
||||
commentBelongToQueue={this.commentBelongToQueue}
|
||||
cleanUpQueue={this.cleanUpQueue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
@@ -350,27 +420,57 @@ const commentConnectionFragment = gql`
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
const withModQueueQuery = withQuery(
|
||||
({queueConfig}) => gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!, $nullStatuses: [COMMENT_STATUS!]) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${Object.keys(queueConfig).map(
|
||||
(queue) => `
|
||||
${queue}: comments(query: {
|
||||
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
: '$nullStatuses'
|
||||
}
|
||||
${
|
||||
queueConfig[queue].tags
|
||||
? `tags: ["${queueConfig[queue].tags.join('", "')}"],`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
queueConfig[queue].action_type
|
||||
? `action_type: ${queueConfig[queue].action_type}`
|
||||
: ''
|
||||
}
|
||||
asset_id: $asset_id,
|
||||
sortOrder: $sortOrder
|
||||
sortOrder: $sortOrder,
|
||||
limit: 20,
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
`)}
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
`
|
||||
)}
|
||||
${Object.keys(queueConfig).map(
|
||||
(queue) => `
|
||||
${queue}Count: commentCount(query: {
|
||||
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
: '$nullStatuses'
|
||||
}
|
||||
${
|
||||
queueConfig[queue].tags
|
||||
? `tags: ["${queueConfig[queue].tags.join('", "')}"],`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
queueConfig[queue].action_type
|
||||
? `action_type: ${queueConfig[queue].action_type}`
|
||||
: ''
|
||||
}
|
||||
asset_id: $asset_id,
|
||||
})
|
||||
`)}
|
||||
`
|
||||
)}
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
@@ -383,24 +483,29 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
organizationName
|
||||
moderation
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
...${getDefinitionName(Comment.fragments.root)}
|
||||
}
|
||||
${Comment.fragments.root}
|
||||
${commentConnectionFragment}
|
||||
`, {
|
||||
options: (props) => {
|
||||
const id = getAssetId(props);
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null,
|
||||
nullStatuses: null,
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
};
|
||||
},
|
||||
});
|
||||
`,
|
||||
{
|
||||
options: (props) => {
|
||||
const id = getAssetId(props);
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null,
|
||||
nullStatuses: null,
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation,
|
||||
@@ -408,22 +513,26 @@ const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
toggleModal,
|
||||
singleView,
|
||||
hideShortcutsNote,
|
||||
toggleStorySearch,
|
||||
viewUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState,
|
||||
notify,
|
||||
}, dispatch),
|
||||
...bindActionCreators(
|
||||
{
|
||||
toggleModal,
|
||||
singleView,
|
||||
hideShortcutsNote,
|
||||
toggleStorySearch,
|
||||
viewUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState,
|
||||
notify,
|
||||
selectCommentId,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetCommentStatus,
|
||||
withModQueueQuery,
|
||||
withModQueueQuery
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -16,16 +16,21 @@ function queueHasComment(root, queue, id) {
|
||||
return root[queue].nodes.find((c) => c.id === id);
|
||||
}
|
||||
|
||||
function removeCommentFromQueue(root, queue, id) {
|
||||
function removeCommentFromQueue(root, queue, id, dangling = false) {
|
||||
if (!queueHasComment(root, queue, id)) {
|
||||
return root;
|
||||
}
|
||||
return update(root, {
|
||||
const changes = {
|
||||
[`${queue}Count`]: {$set: root[`${queue}Count`] - 1},
|
||||
[queue]: {
|
||||
};
|
||||
|
||||
if (!dangling) {
|
||||
changes[queue] = {
|
||||
nodes: {$apply: (nodes) => nodes.filter((c) => c.id !== id)},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
function shouldCommentBeAdded(root, queue, comment, sortOrder) {
|
||||
@@ -40,26 +45,46 @@ function shouldCommentBeAdded(root, queue, comment, sortOrder) {
|
||||
: new Date(comment.created_at) >= cursor;
|
||||
}
|
||||
|
||||
function addCommentToQueue(root, queue, comment, sortOrder) {
|
||||
function addCommentToQueue(root, queue, comment, sortOrder, cleanup) {
|
||||
if (queueHasComment(root, queue, comment.id)) {
|
||||
return root;
|
||||
}
|
||||
|
||||
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
|
||||
const changes = {
|
||||
[`${queue}Count`]: {$set: root[`${queue}Count`] + 1},
|
||||
};
|
||||
|
||||
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
|
||||
const nodes = root[queue].nodes.concat(comment).sort(sortAlgo);
|
||||
changes[queue] = {
|
||||
nodes: {$set: nodes},
|
||||
startCursor: {$set: nodes[0].created_at},
|
||||
endCursor: {$set: nodes[nodes.length - 1].created_at},
|
||||
};
|
||||
if (!shouldCommentBeAdded(root, queue, comment, sortOrder)) {
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
return update(root, changes);
|
||||
const cursor = new Date(root[queue].startCursor);
|
||||
const date = new Date(comment.created_at);
|
||||
|
||||
let append = sortOrder === 'ASC'
|
||||
? date >= cursor
|
||||
: date <= cursor;
|
||||
|
||||
const nodes = append
|
||||
? root[queue].nodes.concat(comment)
|
||||
: [comment].concat(...root[queue].nodes);
|
||||
|
||||
changes[queue] = {
|
||||
nodes: {$set: nodes},
|
||||
};
|
||||
|
||||
const next = update(root, changes);
|
||||
|
||||
if (!cleanup) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return cleanUpQueue(next, queue, sortOrder);
|
||||
}
|
||||
|
||||
function sortComments(nodes, sortOrder) {
|
||||
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
|
||||
return nodes.sort(sortAlgo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,24 +93,92 @@ function addCommentToQueue(root, queue, comment, sortOrder) {
|
||||
function getCommentQueues(comment, queueConfig) {
|
||||
const queues = [];
|
||||
Object.keys(queueConfig).forEach((key) => {
|
||||
const {action_type, statuses, tags} = queueConfig[key];
|
||||
let addToQueues = true;
|
||||
if (statuses && statuses.indexOf(comment.status) === -1) {
|
||||
addToQueues = false;
|
||||
}
|
||||
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
|
||||
addToQueues = false;
|
||||
}
|
||||
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
|
||||
addToQueues = false;
|
||||
}
|
||||
if (addToQueues) {
|
||||
if (commentBelongToQueue(key, comment, queueConfig)) {
|
||||
queues.push(key);
|
||||
}
|
||||
});
|
||||
return queues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the comment belongs to the queue.
|
||||
*/
|
||||
export function commentBelongToQueue(queue, comment, queueConfig) {
|
||||
const {action_type, statuses, tags} = queueConfig[queue];
|
||||
let belong = true;
|
||||
if (statuses && statuses.indexOf(comment.status) === -1) {
|
||||
belong = false;
|
||||
}
|
||||
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
|
||||
belong = false;
|
||||
}
|
||||
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
|
||||
belong = false;
|
||||
}
|
||||
return belong;
|
||||
}
|
||||
|
||||
function isVisible(id) {
|
||||
return !!document.getElementById(`comment_${id}`);
|
||||
}
|
||||
|
||||
function isEndOfListVisible(root, queue) {
|
||||
return root[queue].nodes.length === 0 || !!document.getElementById('end-of-comment-list');
|
||||
}
|
||||
|
||||
function applyCommentChanges(root, comment, queueConfig) {
|
||||
const queues = Object.keys(queueConfig);
|
||||
for (let i = 0; i < queues.length; i++) {
|
||||
const queue = queues[i];
|
||||
const index = root[queue].nodes.findIndex(({id}) => id === comment.id);
|
||||
if (index > -1) {
|
||||
return update(root, {
|
||||
[queue]: {
|
||||
nodes: {
|
||||
[index]: {$merge: comment},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove dangling comments, sort and resize queues.
|
||||
* If queueConfig is omitted, dangling comments are not removed.
|
||||
*/
|
||||
export function cleanUpQueue(root, queue, sortOrder, queueConfig) {
|
||||
let nodes = root[queue].nodes;
|
||||
let hasNextPage = root[queue].hasNextPage;
|
||||
|
||||
if (!nodes.length) {
|
||||
return root;
|
||||
}
|
||||
|
||||
if (queueConfig) {
|
||||
nodes = root[queue].nodes.filter((comment) => commentBelongToQueue(queue, comment, queueConfig));
|
||||
}
|
||||
|
||||
nodes = sortComments(
|
||||
nodes,
|
||||
sortOrder,
|
||||
);
|
||||
|
||||
if (nodes.length > 100) {
|
||||
nodes = nodes.slice(0, 100);
|
||||
hasNextPage = true;
|
||||
}
|
||||
|
||||
return update(root, {
|
||||
[queue]: {
|
||||
nodes: {$set: nodes},
|
||||
endCursor: {$set: nodes[nodes.length - 1].created_at},
|
||||
hasNextPage: {$set: hasNextPage},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assimilate comment changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
@@ -113,25 +206,27 @@ export function handleCommentChange(root, comment, sortOrder, notify, queueConfi
|
||||
Object.keys(queueConfig).forEach((queue) => {
|
||||
if (nextQueues.indexOf(queue) >= 0) {
|
||||
if (!queueHasComment(next, queue, comment.id)) {
|
||||
next = addCommentToQueue(next, queue, comment, sortOrder);
|
||||
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sortOrder)) {
|
||||
next = addCommentToQueue(next, queue, comment, sortOrder, activeQueue !== queue);
|
||||
if (notify && activeQueue === queue && isEndOfListVisible(root, queue)) {
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
} else if(queueHasComment(next, queue, comment.id)){
|
||||
next = removeCommentFromQueue(next, queue, comment.id);
|
||||
if (notify && activeQueue === queue) {
|
||||
const dangling = activeQueue === queue && comment.status_history[comment.status_history.length - 1].assigned_by.id !== root.me.id;
|
||||
next = removeCommentFromQueue(next, queue, comment.id, dangling);
|
||||
if (notify && isVisible(comment.id)) {
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
notify
|
||||
&& queueHasComment(next, queue, comment.id)
|
||||
&& activeQueue === queue
|
||||
) {
|
||||
if (notify && isVisible(comment.id)) {
|
||||
showNotificationOnce();
|
||||
}
|
||||
|
||||
// We need to apply every comment change, because we use
|
||||
// batched subscription handler which bypasses apollo that would
|
||||
// have done that for us.
|
||||
next = applyCommentChanges(next, comment, queueConfig);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -193,7 +193,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
|
||||
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED' || comment.status === 'SYSTEM_WITHHELD') {
|
||||
if (
|
||||
prev.me.roles.indexOf('ADMIN') === -1 && prev.asset.settings.moderation === 'PRE' ||
|
||||
comment.status === 'PREMOD' ||
|
||||
comment.status === 'REJECTED' ||
|
||||
comment.status === 'SYSTEM_WITHHELD'
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return insertCommentIntoEmbedQuery(prev, comment);
|
||||
|
||||
@@ -38,7 +38,7 @@ function applyToCommentsOrigin(root, callback) {
|
||||
function findAndInsertComment(parent, comment) {
|
||||
const isAsset = parent.__typename === 'Asset';
|
||||
const [connectionField, countField, action] = isAsset
|
||||
? ['comments', 'commentCount', '$unshift']
|
||||
? ['comments', 'totalCommentCount', '$unshift']
|
||||
: ['replies', 'replyCount', '$push'];
|
||||
|
||||
if (
|
||||
@@ -67,19 +67,12 @@ function findAndInsertComment(parent, comment) {
|
||||
}
|
||||
|
||||
export function insertCommentIntoEmbedQuery(root, comment) {
|
||||
|
||||
// Increase total comment count by one.
|
||||
root = update(root, {
|
||||
asset: {
|
||||
totalCommentCount: {$apply: (c) => c + 1},
|
||||
},
|
||||
});
|
||||
return applyToCommentsOrigin(root, (origin) => findAndInsertComment(origin, comment));
|
||||
}
|
||||
|
||||
function findAndRemoveComment(parent, id) {
|
||||
const [connectionField, countField] = parent.__typename === 'Asset'
|
||||
? ['comments', 'commentCount']
|
||||
? ['comments', 'totalCommentCount']
|
||||
: ['replies', 'replyCount'];
|
||||
|
||||
const connection = parent[connectionField];
|
||||
@@ -104,13 +97,6 @@ function findAndRemoveComment(parent, id) {
|
||||
}
|
||||
|
||||
export function removeCommentFromEmbedQuery(root, id) {
|
||||
|
||||
// Decrease total comment by one.
|
||||
root = update(root, {
|
||||
asset: {
|
||||
totalCommentCount: {$apply: (c) => c - 1},
|
||||
},
|
||||
});
|
||||
return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id));
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@ const fragments = {
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
roles
|
||||
}
|
||||
settings {
|
||||
organizationName
|
||||
@@ -336,7 +337,6 @@ const fragments = {
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
commentCount @skip(if: $hasComment)
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
@@ -357,7 +357,6 @@ const fragments = {
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
refetching: state.embed.refetching,
|
||||
commentCountCache: state.stream.commentCountCache,
|
||||
activeReplyBox: state.stream.activeReplyBox,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
|
||||
@@ -49,8 +49,9 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
memoized = null;
|
||||
resolvedDocument = null;
|
||||
lastNetworkStatus = null;
|
||||
data = null;
|
||||
name = '';
|
||||
apolloData = null;
|
||||
data = null;
|
||||
|
||||
// Pending subscription data.
|
||||
subscriptionQueue = [];
|
||||
@@ -83,6 +84,9 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
// Handle any pending susbcription data in the subscription queue at max once every second.
|
||||
// Updates are batched in written into apollo in one go.
|
||||
processSubscriptionQueue = throttle(() => {
|
||||
if (!this.subscriptionQueue.length) {
|
||||
return;
|
||||
}
|
||||
const variables = typeof this.wrappedOptions === 'function'
|
||||
? this.wrappedOptions(this.props).variables
|
||||
: this.wrappedOptions.variables;
|
||||
@@ -151,6 +155,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
};
|
||||
|
||||
nextData(data) {
|
||||
this.apolloData = data;
|
||||
this.emitWhenNeeded(data);
|
||||
|
||||
// If data was previously set, we update it in a immutable way.
|
||||
@@ -181,16 +186,24 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
variables: data.variables,
|
||||
networkStatus: data.networkStatus,
|
||||
loading: data.loading,
|
||||
startPolling: data.startPolling,
|
||||
stopPolling: data.stopPolling,
|
||||
refetch: data.refetch,
|
||||
updateQuery: data.updateQuery,
|
||||
subscribeToMoreThrottled: this.subscribeToMoreThrottled,
|
||||
startPolling: (...args) => {
|
||||
return this.apolloData.startPolling(...args);
|
||||
},
|
||||
stopPolling: (...args) => {
|
||||
return this.apolloData.stopPolling(...args);
|
||||
},
|
||||
updateQuery: (...args) => {
|
||||
return this.apolloData.updateQuery(...args);
|
||||
},
|
||||
refetch: (...args) => {
|
||||
return this.apolloData.refetch(...args);
|
||||
},
|
||||
subscribeToMore: (stmArgs) => {
|
||||
const resolvedDocument = this.resolveDocument(stmArgs.document);
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return data.subscribeToMore({
|
||||
return this.apolloData.subscribeToMore({
|
||||
...stmArgs,
|
||||
document: resolvedDocument,
|
||||
onError: (err) => {
|
||||
@@ -209,7 +222,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
{variables: lmArgs.variables});
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return data.fetchMore({
|
||||
return this.apolloData.fetchMore({
|
||||
...lmArgs,
|
||||
query: resolvedDocument,
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ class ProfileContainer extends Component {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (loading || !me) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
.dialog {
|
||||
}
|
||||
|
||||
:global(.backdrop) {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: black;
|
||||
opacity: 0.1;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dialogPolyfill from 'dialog-polyfill';
|
||||
import 'dialog-polyfill/dialog-polyfill.css';
|
||||
import styles from './Dialog.css';
|
||||
import {Portal} from 'react-portal';
|
||||
|
||||
export default class Dialog extends Component {
|
||||
static propTypes = {
|
||||
@@ -52,13 +54,15 @@ export default class Dialog extends Component {
|
||||
const {children, className = '', onClose, onCancel, open, ...rest} = this.props; // eslint-disable-line
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={(el) => { this.dialog = el; }}
|
||||
className={`mdl-dialog ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</dialog>
|
||||
<Portal>
|
||||
<dialog
|
||||
ref={(el) => { this.dialog = el; }}
|
||||
className={`mdl-dialog ${className} ${styles.dialog}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user