mirror of
https://github.com/wassname/talk.git
synced 2026-07-11 04:27:01 +08:00
Merge branch 'master' into e2e-fixes
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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ 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();
|
||||
}
|
||||
};
|
||||
|
||||
showSuspendUserDialog = () => {
|
||||
const {comment, showSuspendUserDialog} = this.props;
|
||||
return showSuspendUserDialog({
|
||||
@@ -55,6 +65,12 @@ class Comment extends React.Component {
|
||||
: this.props.rejectComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
if (!prev.selected && this.props.selected) {
|
||||
this.ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
comment,
|
||||
@@ -65,6 +81,8 @@ class Comment extends React.Component {
|
||||
root: {settings},
|
||||
currentUserId,
|
||||
currentAsset,
|
||||
clearHeightCache,
|
||||
dangling,
|
||||
} = this.props;
|
||||
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
@@ -73,8 +91,11 @@ 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}>
|
||||
@@ -114,6 +135,7 @@ class Comment extends React.Component {
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
@@ -145,6 +167,7 @@ class Comment extends React.Component {
|
||||
<Slot
|
||||
fill="adminCommentContent"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<div className={styles.sideActions}>
|
||||
@@ -166,6 +189,7 @@ class Comment extends React.Component {
|
||||
<Slot
|
||||
fill="adminSideActions"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
@@ -176,6 +200,7 @@ class Comment extends React.Component {
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
clearHeightCache={clearHeightCache}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -185,12 +210,16 @@ 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,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
|
||||
@@ -12,15 +12,10 @@ 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,8 +25,6 @@ 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()
|
||||
@@ -74,8 +67,7 @@ class Moderation extends Component {
|
||||
}
|
||||
|
||||
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){
|
||||
@@ -96,74 +88,16 @@ class Moderation extends Component {
|
||||
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;
|
||||
this.setState({isLoadingMore: false});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -176,56 +110,12 @@ 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}`));
|
||||
}
|
||||
|
||||
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;
|
||||
const {asset} = root;
|
||||
@@ -266,17 +156,22 @@ 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}
|
||||
selectedCommentId={moderation.selectedCommentId}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
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}
|
||||
@@ -307,12 +202,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,
|
||||
showBanUserDialog: 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,29 @@ 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 +43,306 @@ 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;
|
||||
}
|
||||
|
||||
let idCursors = this.state.idCursors;
|
||||
|
||||
// Comments have been removed.
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
nextComments.length < prevComments.length
|
||||
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));
|
||||
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));
|
||||
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}
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
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 () {
|
||||
const {
|
||||
comments,
|
||||
selectedCommentId,
|
||||
commentCount,
|
||||
singleView,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
@@ -159,54 +377,35 @@ 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}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -216,14 +415,19 @@ ModerationQueue.propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
selectCommentId: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
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,6 +8,7 @@
|
||||
overflow: visible;
|
||||
height: 144px;
|
||||
min-height: auto;
|
||||
margin-top: 16px;
|
||||
z-index: 10;
|
||||
|
||||
@media (--tablet) {
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
toggleStorySearch,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState
|
||||
clearState,
|
||||
selectCommentId,
|
||||
} from 'actions/moderation';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
@@ -168,6 +169,14 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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'});
|
||||
}
|
||||
@@ -176,9 +185,13 @@ class ModerationContainer extends Component {
|
||||
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,
|
||||
@@ -194,7 +207,6 @@ class ModerationContainer extends Component {
|
||||
[tab]: {
|
||||
nodes: {$push: comments.nodes},
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
startCursor: {$set: comments.startCursor},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
},
|
||||
});
|
||||
@@ -246,6 +258,9 @@ class ModerationContainer extends Component {
|
||||
activeTab={this.activeTab}
|
||||
queueConfig={currentQueueConfig}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
selectedCommentId={this.props.selectedCommentId}
|
||||
commentBelongToQueue={this.commentBelongToQueue}
|
||||
cleanUpQueue={this.cleanUpQueue}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -361,7 +376,8 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
${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
|
||||
}
|
||||
@@ -386,6 +402,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
organizationName
|
||||
moderation
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
...${getDefinitionName(Comment.fragments.root)}
|
||||
}
|
||||
${Comment.fragments.root}
|
||||
@@ -423,6 +442,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
storySearchChange,
|
||||
clearState,
|
||||
notify,
|
||||
selectCommentId,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
||||
@@ -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,88 @@ 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 (queueConfig) {
|
||||
nodes = root[queue].nodes.filter((comment) => commentBelongToQueue(queue, comment, queueConfig));
|
||||
}
|
||||
|
||||
nodes = sortComments(
|
||||
nodes,
|
||||
sortOrder,
|
||||
);
|
||||
|
||||
if (nodes.length > 2) {
|
||||
nodes = nodes.slice(0, 2);
|
||||
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 +202,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;
|
||||
}
|
||||
|
||||
@@ -84,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;
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"react-test-renderer": "15.5",
|
||||
"react-toastify": "^1.5.0",
|
||||
"react-transition-group": "^1.1.3",
|
||||
"react-virtualized": "9.13.0",
|
||||
"recompose": "^0.23.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
|
||||
@@ -65,6 +65,14 @@ const COMMENT_FEATURED_SUBSCRIPTION = gql`
|
||||
commentFeatured(asset_id: $assetId) {
|
||||
comment {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
user {
|
||||
id
|
||||
|
||||
@@ -1045,7 +1045,7 @@ babel-register@^6.26.0:
|
||||
mkdirp "^0.5.1"
|
||||
source-map-support "^0.4.15"
|
||||
|
||||
babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1:
|
||||
babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
||||
dependencies:
|
||||
@@ -2435,7 +2435,7 @@ doctypes@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
||||
|
||||
dom-helpers@^3.2.0:
|
||||
"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
|
||||
|
||||
@@ -5504,7 +5504,7 @@ longest@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
|
||||
dependencies:
|
||||
@@ -7535,6 +7535,16 @@ react-transition-group@^1.1.2, react-transition-group@^1.1.3:
|
||||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-virtualized@9.13.0:
|
||||
version "9.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.13.0.tgz#83e4d984271a37631225e5fe6faeaeada6e59f53"
|
||||
dependencies:
|
||||
babel-runtime "^6.23.0"
|
||||
classnames "^2.2.3"
|
||||
dom-helpers "^2.4.0 || ^3.0.0"
|
||||
loose-envify "^1.3.0"
|
||||
prop-types "^15.5.4"
|
||||
|
||||
react@^15.3.1, react@^15.4.2:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
|
||||
|
||||
Reference in New Issue
Block a user