diff --git a/package-lock.json b/package-lock.json index 7713003706..027d5c7b9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coralproject/talk", - "version": "8.2.3", + "version": "8.2.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@coralproject/talk", - "version": "8.2.3", + "version": "8.2.4", "license": "Apache-2.0", "dependencies": { "@ampproject/toolbox-cache-url": "^2.9.0", diff --git a/package.json b/package.json index 83f86c2db2..e5df079d95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "8.2.3", + "version": "8.2.4", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/src/core/client/admin/components/BanModal.tsx b/src/core/client/admin/components/BanModal.tsx index 092e8fe238..d02e722c9c 100644 --- a/src/core/client/admin/components/BanModal.tsx +++ b/src/core/client/admin/components/BanModal.tsx @@ -222,7 +222,7 @@ const BanModal: FunctionComponent = ({ message: customizeMessage ? emailMessage : getDefaultMessage, rejectExistingComments, siteIDs: viewerIsScoped - ? viewer.moderationScopes!.sites!.map(({ id }) => id) + ? viewer?.moderationScopes?.sites?.map(({ id }) => id) : [], }); break; diff --git a/src/core/client/admin/components/ModerateCard/ModerateCard.tsx b/src/core/client/admin/components/ModerateCard/ModerateCard.tsx index ab61d7008a..f8cde3c34b 100644 --- a/src/core/client/admin/components/ModerateCard/ModerateCard.tsx +++ b/src/core/client/admin/components/ModerateCard/ModerateCard.tsx @@ -1,5 +1,7 @@ import { Localized } from "@fluent/react/compat"; import cn from "classnames"; +import key from "keymaster"; +import { noop } from "lodash"; import React, { FunctionComponent, useCallback, @@ -9,6 +11,7 @@ import React, { } from "react"; import { MediaContainer } from "coral-admin/components/MediaContainer"; +import { HOTKEYS } from "coral-admin/constants"; import { GQLWordlistMatch } from "coral-framework/schema"; import { PropTypesOf } from "coral-framework/types"; import { @@ -133,6 +136,33 @@ const ModerateCard: FunctionComponent = ({ }) => { const div = useRef(null); + useEffect(() => { + if (selected) { + key.setScope(id); + } + }, [selected, id]); + + useEffect(() => { + if (selectNext) { + key(HOTKEYS.NEXT, id, selectNext); + } + if (selectPrev) { + key(HOTKEYS.PREV, id, selectPrev); + } + if (onBan) { + key(HOTKEYS.BAN, id, onBan); + } + key(HOTKEYS.APPROVE, id, onApprove); + key(HOTKEYS.REJECT, id, onReject); + + return () => { + // Remove all events that are set in the ${id} scope. + key.deleteScope(id); + }; + + return noop; + }, [id, selectNext, selectPrev, onBan, onApprove, onReject]); + useEffect(() => { if (selected && div && div.current) { div.current.focus(); diff --git a/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx b/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx index 5a1bcfe2e3..8f7a4625f2 100644 --- a/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx +++ b/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx @@ -30,8 +30,8 @@ import { import { ModerateCardContainer_settings } from "coral-admin/__generated__/ModerateCardContainer_settings.graphql"; import { ModerateCardContainer_viewer } from "coral-admin/__generated__/ModerateCardContainer_viewer.graphql"; import { ModerateCardContainerLocal } from "coral-admin/__generated__/ModerateCardContainerLocal.graphql"; - import { UserStatusChangeContainer_viewer } from "coral-admin/__generated__/UserStatusChangeContainer_viewer.graphql"; + import FeatureCommentMutation from "./FeatureCommentMutation"; import ModerateCard from "./ModerateCard"; import ModeratedByContainer from "./ModeratedByContainer"; @@ -52,6 +52,7 @@ interface Props { selectPrev?: () => void; selectNext?: () => void; loadNext?: (() => void) | null; + onModerated?: () => void; } function getStatus(comment: ModerateCardContainer_comment) { @@ -84,6 +85,7 @@ const ModerateCardContainer: FunctionComponent = ({ onConversationClicked: conversationClicked, onSetSelected: setSelected, loadNext, + onModerated, }) => { const approveComment = useMutation(ApproveCommentMutation); const rejectComment = useMutation(RejectCommentMutation); @@ -129,6 +131,9 @@ const ModerateCardContainer: FunctionComponent = ({ if (loadNext) { loadNext(); } + if (onModerated) { + onModerated(); + } }, [ approveComment, comment.id, @@ -137,6 +142,7 @@ const ModerateCardContainer: FunctionComponent = ({ match, readOnly, moderationQueueSort, + onModerated, ]); const handleReject = useCallback(async () => { @@ -161,6 +167,9 @@ const ModerateCardContainer: FunctionComponent = ({ if (loadNext) { loadNext(); } + if (onModerated) { + onModerated(); + } }, [ comment.revision, comment.id, @@ -169,6 +178,7 @@ const ModerateCardContainer: FunctionComponent = ({ rejectComment, loadNext, moderationQueueSort, + onModerated, ]); const handleFeature = useCallback(() => { diff --git a/src/core/client/admin/routes/Moderate/Queue/Queue.tsx b/src/core/client/admin/routes/Moderate/Queue/Queue.tsx index 8fc4a7878a..86b4c28ca5 100644 --- a/src/core/client/admin/routes/Moderate/Queue/Queue.tsx +++ b/src/core/client/admin/routes/Moderate/Queue/Queue.tsx @@ -1,6 +1,5 @@ import { Localized } from "@fluent/react/compat"; import key from "keymaster"; -import { noop } from "lodash"; import React, { FunctionComponent, useCallback, @@ -40,8 +39,6 @@ interface Props { viewNewCount?: number; } -const QUEUE_HOTKEY_ID = "moderation-queue"; - const Queue: FunctionComponent = ({ settings, comments, @@ -63,8 +60,16 @@ const Queue: FunctionComponent = ({ const [conversationModalVisible, setConversationModalVisible] = useState(false); const [conversationCommentID, setConversationCommentID] = useState(""); + const [hasModerated, setHasModerated] = useState(false); const memoize = useMemoizer(); + // So we can register hotkeys for the first comment without immediately pulling focus + useEffect(() => { + if (comments.length > 0) { + key.setScope(comments[0].id); + } + }, []); + const toggleView = useCallback(() => { if (!singleView) { setSelectedComment(0); @@ -81,49 +86,38 @@ const Queue: FunctionComponent = ({ const commentsRef = useRef(comments); commentsRef.current = comments; - const selectNext = useCallback(() => { - const index = selectedCommentRef.current || 0; - const nextComment = commentsRef.current[index + 1]; - if (nextComment) { - setSelectedComment(index + 1); - const container: HTMLElement | null = window.document.getElementById( - `moderate-comment-${nextComment.id}` - ); - if (container) { - container.scrollIntoView(); + useEffect(() => { + if (selectedComment !== null && commentsRef.current.length > 0) { + // We've moderated the last comment via hotkey + if (selectedComment >= commentsRef.current.length) { + setSelectedComment(commentsRef.current.length - 1); + // We've moderated the first comment via hotkey + } else if (selectedComment < 0 && hasModerated) { + setSelectedComment(0); } } - }, [window.document]); - - const selectPrev = useCallback(() => { - const index = selectedCommentRef.current || 0; - const prevComment = commentsRef.current[index - 1]; - if (prevComment) { - setSelectedComment(index - 1); - const container: HTMLElement | null = window.document.getElementById( - `moderate-comment-${prevComment.id}` - ); - if (container) { - container.scrollIntoView(); + }, [commentsRef.current.length, selectedComment, hasModerated]); + + const varyComment = useCallback( + (delta: number) => { + const index = selectedCommentRef.current || 0; + const targetComment = commentsRef.current[index + delta]; + if (targetComment) { + setSelectedComment(index + delta); + const container: HTMLElement | null = window.document.getElementById( + `moderate-comment-${targetComment.id}` + ); + if (container) { + container.scrollIntoView(); + } } - } - }, [window.document]); - - useEffect(() => { - key(HOTKEYS.NEXT, QUEUE_HOTKEY_ID, selectNext); - key(HOTKEYS.PREV, QUEUE_HOTKEY_ID, selectPrev); - - // The the scope such that only events attached to the ${id} scope will - // be honored. - key.setScope(QUEUE_HOTKEY_ID); + }, + [window.document] + ); - return () => { - // Remove all events that are set in the ${id} scope. - key.deleteScope(QUEUE_HOTKEY_ID); - }; + const selectNext = useCallback(() => varyComment(1), [varyComment]); - return noop; - }, [selectNext, selectPrev]); + const selectPrev = useCallback(() => varyComment(-1), [varyComment]); const onSetUserDrawerUserID = useCallback((userID: string) => { setUserDrawerID(userID); @@ -189,6 +183,7 @@ const Queue: FunctionComponent = ({ selected={selectedComment === i} selectPrev={selectPrev} selectNext={selectNext} + onModerated={() => setHasModerated(true)} /> )} />