diff --git a/src/components/edit-menu/__tests__/edit-menu.test.tsx b/src/components/edit-menu/__tests__/edit-menu.test.tsx index 85fe46fb..35d7b5db 100644 --- a/src/components/edit-menu/__tests__/edit-menu.test.tsx +++ b/src/components/edit-menu/__tests__/edit-menu.test.tsx @@ -376,6 +376,8 @@ describe('EditMenu', () => { await renderMenu(basePost); await openMenu(); + expect(getLabelCheckbox('Edit?')).toBeNull(); + await click(getCheckbox('removed')); await click(getCheckbox('purged')); await click(getCheckbox('locked')); @@ -448,6 +450,41 @@ describe('EditMenu', () => { expect(testState.modOptions?.commentModeration?.author).toBeUndefined(); }); + it('lets moderators edit their own post content from the mod menu', async () => { + testState.privileges = { + isAccountCommentAuthor: true, + isAccountMod: true, + isCommentAuthorMod: false, + }; + + await renderMenu(basePost); + await openMenu(); + + const editCheckbox = getLabelCheckbox('Edit?'); + expect(editCheckbox).not.toBeNull(); + + await click(editCheckbox); + + const textarea = container.querySelector('textarea'); + expect(textarea).not.toBeNull(); + + await dispatchInput(textarea as HTMLTextAreaElement, 'Updated by moderator'); + await clickButton('save'); + + expect(testState.publishAuthorEditMock).toHaveBeenCalledOnce(); + expect(testState.publishCommentModerationMock).toHaveBeenCalledOnce(); + expect(testState.authorOptions).toMatchObject({ + author: { + address: '0xauthor', + displayName: 'Alice', + }, + commentCid: 'comment-1', + content: 'Updated by moderator', + deleted: false, + communityAddress: 'music-posting.eth', + }); + }); + it('does not enable purge when the confirmation is rejected', async () => { confirmSpy.mockReturnValue(false); testState.privileges = { diff --git a/src/components/edit-menu/edit-menu.module.css b/src/components/edit-menu/edit-menu.module.css index 4681a582..e63a687d 100644 --- a/src/components/edit-menu/edit-menu.module.css +++ b/src/components/edit-menu/edit-menu.module.css @@ -42,7 +42,7 @@ } -.menuItem input[type="text"] { +.menuItem input[type="text"], .editTextarea { padding: 2px; box-shadow: var(--box-shadow-input); margin-top: 2px; @@ -50,12 +50,12 @@ margin-bottom: 2px; } -.menuItem input[type="text"], .menuItem input[type='number'] { +.menuItem input[type="text"], .menuItem input[type='number'], .editTextarea { border: var(--post-form-field-input-border); outline: none; } -.menuItem input[type="text"]:focus, .menuItem input[type='number']:focus { +.menuItem input[type="text"]:focus, .menuItem input[type='number']:focus, .editTextarea:focus { border: var(--post-form-field-input-focus-border); } @@ -64,6 +64,13 @@ display: block; } +.editTextarea { + width: calc(100% - 12px); + height: 100px; + margin-left: 3px; + font-family: inherit; +} + .bottom button { text-transform: capitalize; margin-left: 2px; diff --git a/src/components/edit-menu/edit-menu.tsx b/src/components/edit-menu/edit-menu.tsx index 5876cc06..b3a750c2 100644 --- a/src/components/edit-menu/edit-menu.tsx +++ b/src/components/edit-menu/edit-menu.tsx @@ -34,7 +34,7 @@ const EditMenu = ({ post }: { post: Comment }) => { const { t } = useTranslation(); const isMobile = useIsMobile(); const resolvedPost = withResolvedCommentCommunityAddress(post); - const { author, cid, deleted, locked, parentCid, pinned, postCid, reason, removed, spoiler } = resolvedPost || {}; + const { author, cid, content, deleted, locked, parentCid, pinned, postCid, reason, removed, spoiler } = resolvedPost || {}; const communityAddress = getCommentCommunityAddress(resolvedPost); const archived = isCommentArchived(resolvedPost); const authorDisplayName = resolvedPost?.author?.displayName; @@ -52,7 +52,9 @@ const EditMenu = ({ post }: { post: Comment }) => { const allowsPseudonymousDelete = pseudonymityMode !== undefined && pseudonymityMode !== 'none'; const canAttemptAuthorDelete = isAccountCommentAuthor || allowsPseudonymousDelete; const canOpenEditMenu = isAccountMod || canAttemptAuthorDelete; + const canEditOwnModPost = isAccountMod && isAccountCommentAuthor; const signer = isAccountCommentAuthor ? account?.signer : undefined; + const [isContentEditorOpen, setIsContentEditorOpen] = useState(false); const latestPostRef = useRef(resolvedPost); useEffect(() => { latestPostRef.current = resolvedPost; @@ -64,6 +66,7 @@ const EditMenu = ({ post }: { post: Comment }) => { commentCid: cid, communityAddress, // Author edit properties + content: canEditOwnModPost ? content : undefined, deleted: canAttemptAuthorDelete ? (deleted ?? false) : undefined, // Mod edit properties commentModeration: isAccountMod @@ -87,9 +90,11 @@ const EditMenu = ({ post }: { post: Comment }) => { }; }, [ isAccountMod, + canEditOwnModPost, canAttemptAuthorDelete, archived, cid, + content, deleted, locked, pinned, @@ -109,6 +114,7 @@ const EditMenu = ({ post }: { post: Comment }) => { const options: PublishCommentEditOptions = { commentCid: cid, communityAddress, + content: canEditOwnModPost ? publishCommentEditOptions.content : undefined, deleted: publishCommentEditOptions.deleted, onChallenge, onChallengeVerification: alertChallengeVerificationFailed, @@ -127,7 +133,18 @@ const EditMenu = ({ post }: { post: Comment }) => { signer, author: signer?.address === author?.address ? { address: signer?.address, displayName: authorDisplayName } : account?.author, }; - }, [publishCommentEditOptions, cid, communityAddress, isAccountCommentAuthor, signer, account?.author, author?.address, authorDisplayName, onChallenge]); + }, [ + publishCommentEditOptions, + cid, + communityAddress, + canEditOwnModPost, + isAccountCommentAuthor, + signer, + account?.author, + author?.address, + authorDisplayName, + onChallenge, + ]); const modEditOptions = useMemo( () => ({ @@ -163,6 +180,7 @@ const EditMenu = ({ post }: { post: Comment }) => { const resetMenuState = () => { setPublishCommentEditOptions(defaultPublishEditOptions); + setIsContentEditorOpen(false); setBanDuration( defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1, ); @@ -250,9 +268,10 @@ const EditMenu = ({ post }: { post: Comment }) => { const headingId = useId(); const hasDeleteStateChanged = (deleted ?? false) !== (publishCommentEditOptions.deleted ?? false); + const hasContentStateChanged = canEditOwnModPost && (content ?? '') !== (publishCommentEditOptions.content ?? ''); const _publishCommentEdit = async () => { - const shouldPublishAuthorEdit = canAttemptAuthorDelete && hasDeleteStateChanged; + const shouldPublishAuthorEdit = (canAttemptAuthorDelete && hasDeleteStateChanged) || hasContentStateChanged; try { if (shouldPublishAuthorEdit && isAccountMod) { @@ -318,6 +337,35 @@ const EditMenu = ({ post }: { post: Comment }) => { )} {isAccountMod && ( <> + {canEditOwnModPost && ( + <> +
+ +
+ {isContentEditorOpen && ( +
+