diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index a3ff2ca3e..18e31091f 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -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, +}); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index cae13947d..8eb939d76 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -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'; diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 43ae81573..6a64b380a 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -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; } diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.css b/client/coral-admin/src/routes/Moderation/components/Comment.css index 029a8e5d1..708761b95 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.css +++ b/client/coral-admin/src/routes/Moderation/components/Comment.css @@ -12,6 +12,7 @@ padding: 10px 0; margin-top: 13px; min-height: 0; + outline: 0; /* Fix rendering issues in Safari by promoting this diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 9f913695a..4c08f1559 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -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, @@ -76,6 +92,9 @@ class Comment extends React.Component { tabIndex={0} className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected}, 'talk-admin-moderate-comment')} id={`comment_${comment.id}`} + onClick={this.handleFocusOrClick} + ref={this.handleRef} + onFocus={this.handleFocusOrClick} >
@@ -190,7 +209,9 @@ class Comment extends React.Component { Comment.propTypes = { viewUserDetail: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, + selectComment: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, + onClick: PropTypes.func, className: PropTypes.string, currentAsset: PropTypes.object, showBanUserDialog: PropTypes.func.isRequired, diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index 392db36e3..594d5d57f 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -12,15 +12,8 @@ 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 = {}; componentWillMount() { const {toggleModal, singleView} = this.props; @@ -30,8 +23,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() @@ -96,64 +87,6 @@ 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; @@ -176,8 +109,6 @@ 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() @@ -186,11 +117,8 @@ class Moderation extends Component { componentWillReceiveProps(nextProps) { - if (this.props.activeTab !== nextProps.activeTab) { - - // Reset selection when changing tabs. - this.select(true, nextProps, null); - } else { + // TODO: Adapt to react virtualized. + if (this.props.activeTab === nextProps.activeTab) { // Detect if comment has left the queue and find next or prev selected comment to set it // as the new selectedCommentId. @@ -218,14 +146,6 @@ class Moderation extends Component { } } - 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; @@ -268,7 +188,7 @@ class Moderation extends Component { comments={comments.nodes} activeTab={activeTab} singleView={moderation.singleView} - selectedCommentId={this.state.selectedCommentId} + selectedCommentId={moderation.selectedCommentId} showBanUserDialog={props.showBanUserDialog} showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} @@ -277,6 +197,7 @@ class Moderation extends Component { commentCount={activeTabCount} currentUserId={this.props.auth.user.id} viewUserDetail={viewUserDetail} + selectCommentId={props.selectCommentId} /> nodes.some((node) => node.id === id); @@ -75,6 +76,46 @@ class ModerationQueue extends React.Component { } throw new Error(`unknown index ${index}`); }; + const view = this.getVisibleComments(); + if (view.length) { + props.selectCommentId(view[0].id); + } + } + + componentDidMount() { + key('j', () => this.selectDown()); + key('k', () => this.selectUp()); + } + + componentWillUnmount() { + key.unbind('j'); + key.unbind('k'); + } + + async selectDown() { + const view = this.getVisibleComments(); + const index = view.findIndex(({id}) => id === this.props.selectedCommentId); + if (index === view.length - 1 && this.props.comments.length !== 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.getVisibleComments(); + 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); + } } componentDidUpdate (prev) { @@ -86,6 +127,15 @@ class ModerationQueue extends React.Component { if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) { this.props.loadMore(); } + + // Scroll to selected comment. + if (prev.selectedCommentId !== this.props.selectedCommentId) { + + const view = this.getVisibleComments(); + const index = view.findIndex(({id}) => id === this.props.selectedCommentId); + + this.listRef.scrollToRow(index); + } } handleListRef = (list) => { @@ -118,8 +168,11 @@ class ModerationQueue extends React.Component { } } - viewNewComments = () => { - this.setState(resetCursors, () => this.reflowList()); + viewNewComments = (callback) => { + this.setState(resetCursors, () => { + this.reflowList(); + callback && callback(); + }); }; reflowList = throttle(() => { @@ -203,6 +256,7 @@ class ModerationQueue extends React.Component { currentAsset={this.props.currentAsset} currentUserId={this.props.currentUserId} clearHeightCache={() => {this.cache.clear(index); this.listRef.recomputeRowHeights(index); }} + selectComment={() => this.props.selectCommentId(comment.id)} />
@@ -244,6 +298,7 @@ class ModerationQueue extends React.Component { rejectComment={props.rejectComment} currentAsset={props.currentAsset} currentUserId={this.props.currentUserId} + selectComment={() => this.props.selectCommentId(comment.id)} />;
); @@ -287,6 +342,7 @@ 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, diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index b936b4c30..d858cc856 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -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'; @@ -246,6 +247,7 @@ class ModerationContainer extends Component { activeTab={this.activeTab} queueConfig={currentQueueConfig} handleCommentChange={this.handleCommentChange} + selectedCommentId={this.props.selectedCommentId} />; } } @@ -423,6 +425,7 @@ const mapDispatchToProps = (dispatch) => ({ storySearchChange, clearState, notify, + selectCommentId, }, dispatch), });