mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(edit-menu): let moderators edit their own post content
Show a content editor in the mod menu when the signed-in mod is the post author, and publish author edits alongside moderation actions on save.
This commit is contained in:
@@ -376,6 +376,8 @@ describe('EditMenu', () => {
|
|||||||
await renderMenu(basePost);
|
await renderMenu(basePost);
|
||||||
await openMenu();
|
await openMenu();
|
||||||
|
|
||||||
|
expect(getLabelCheckbox('Edit?')).toBeNull();
|
||||||
|
|
||||||
await click(getCheckbox('removed'));
|
await click(getCheckbox('removed'));
|
||||||
await click(getCheckbox('purged'));
|
await click(getCheckbox('purged'));
|
||||||
await click(getCheckbox('locked'));
|
await click(getCheckbox('locked'));
|
||||||
@@ -448,6 +450,41 @@ describe('EditMenu', () => {
|
|||||||
expect(testState.modOptions?.commentModeration?.author).toBeUndefined();
|
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 () => {
|
it('does not enable purge when the confirmation is rejected', async () => {
|
||||||
confirmSpy.mockReturnValue(false);
|
confirmSpy.mockReturnValue(false);
|
||||||
testState.privileges = {
|
testState.privileges = {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.menuItem input[type="text"] {
|
.menuItem input[type="text"], .editTextarea {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
box-shadow: var(--box-shadow-input);
|
box-shadow: var(--box-shadow-input);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
@@ -50,12 +50,12 @@
|
|||||||
margin-bottom: 2px;
|
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);
|
border: var(--post-form-field-input-border);
|
||||||
outline: none;
|
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);
|
border: var(--post-form-field-input-focus-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +64,13 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editTextarea {
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
height: 100px;
|
||||||
|
margin-left: 3px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom button {
|
.bottom button {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const resolvedPost = withResolvedCommentCommunityAddress(post);
|
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 communityAddress = getCommentCommunityAddress(resolvedPost);
|
||||||
const archived = isCommentArchived(resolvedPost);
|
const archived = isCommentArchived(resolvedPost);
|
||||||
const authorDisplayName = resolvedPost?.author?.displayName;
|
const authorDisplayName = resolvedPost?.author?.displayName;
|
||||||
@@ -52,7 +52,9 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
const allowsPseudonymousDelete = pseudonymityMode !== undefined && pseudonymityMode !== 'none';
|
const allowsPseudonymousDelete = pseudonymityMode !== undefined && pseudonymityMode !== 'none';
|
||||||
const canAttemptAuthorDelete = isAccountCommentAuthor || allowsPseudonymousDelete;
|
const canAttemptAuthorDelete = isAccountCommentAuthor || allowsPseudonymousDelete;
|
||||||
const canOpenEditMenu = isAccountMod || canAttemptAuthorDelete;
|
const canOpenEditMenu = isAccountMod || canAttemptAuthorDelete;
|
||||||
|
const canEditOwnModPost = isAccountMod && isAccountCommentAuthor;
|
||||||
const signer = isAccountCommentAuthor ? account?.signer : undefined;
|
const signer = isAccountCommentAuthor ? account?.signer : undefined;
|
||||||
|
const [isContentEditorOpen, setIsContentEditorOpen] = useState(false);
|
||||||
const latestPostRef = useRef(resolvedPost);
|
const latestPostRef = useRef(resolvedPost);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latestPostRef.current = resolvedPost;
|
latestPostRef.current = resolvedPost;
|
||||||
@@ -64,6 +66,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
commentCid: cid,
|
commentCid: cid,
|
||||||
communityAddress,
|
communityAddress,
|
||||||
// Author edit properties
|
// Author edit properties
|
||||||
|
content: canEditOwnModPost ? content : undefined,
|
||||||
deleted: canAttemptAuthorDelete ? (deleted ?? false) : undefined,
|
deleted: canAttemptAuthorDelete ? (deleted ?? false) : undefined,
|
||||||
// Mod edit properties
|
// Mod edit properties
|
||||||
commentModeration: isAccountMod
|
commentModeration: isAccountMod
|
||||||
@@ -87,9 +90,11 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
isAccountMod,
|
isAccountMod,
|
||||||
|
canEditOwnModPost,
|
||||||
canAttemptAuthorDelete,
|
canAttemptAuthorDelete,
|
||||||
archived,
|
archived,
|
||||||
cid,
|
cid,
|
||||||
|
content,
|
||||||
deleted,
|
deleted,
|
||||||
locked,
|
locked,
|
||||||
pinned,
|
pinned,
|
||||||
@@ -109,6 +114,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
const options: PublishCommentEditOptions = {
|
const options: PublishCommentEditOptions = {
|
||||||
commentCid: cid,
|
commentCid: cid,
|
||||||
communityAddress,
|
communityAddress,
|
||||||
|
content: canEditOwnModPost ? publishCommentEditOptions.content : undefined,
|
||||||
deleted: publishCommentEditOptions.deleted,
|
deleted: publishCommentEditOptions.deleted,
|
||||||
onChallenge,
|
onChallenge,
|
||||||
onChallengeVerification: alertChallengeVerificationFailed,
|
onChallengeVerification: alertChallengeVerificationFailed,
|
||||||
@@ -127,7 +133,18 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
signer,
|
signer,
|
||||||
author: signer?.address === author?.address ? { address: signer?.address, displayName: authorDisplayName } : account?.author,
|
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<PublishCommentModerationOptions>(
|
const modEditOptions = useMemo<PublishCommentModerationOptions>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -163,6 +180,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
|
|
||||||
const resetMenuState = () => {
|
const resetMenuState = () => {
|
||||||
setPublishCommentEditOptions(defaultPublishEditOptions);
|
setPublishCommentEditOptions(defaultPublishEditOptions);
|
||||||
|
setIsContentEditorOpen(false);
|
||||||
setBanDuration(
|
setBanDuration(
|
||||||
defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1,
|
defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1,
|
||||||
);
|
);
|
||||||
@@ -250,9 +268,10 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
|
|
||||||
const headingId = useId();
|
const headingId = useId();
|
||||||
const hasDeleteStateChanged = (deleted ?? false) !== (publishCommentEditOptions.deleted ?? false);
|
const hasDeleteStateChanged = (deleted ?? false) !== (publishCommentEditOptions.deleted ?? false);
|
||||||
|
const hasContentStateChanged = canEditOwnModPost && (content ?? '') !== (publishCommentEditOptions.content ?? '');
|
||||||
|
|
||||||
const _publishCommentEdit = async () => {
|
const _publishCommentEdit = async () => {
|
||||||
const shouldPublishAuthorEdit = canAttemptAuthorDelete && hasDeleteStateChanged;
|
const shouldPublishAuthorEdit = (canAttemptAuthorDelete && hasDeleteStateChanged) || hasContentStateChanged;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (shouldPublishAuthorEdit && isAccountMod) {
|
if (shouldPublishAuthorEdit && isAccountMod) {
|
||||||
@@ -318,6 +337,35 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
)}
|
)}
|
||||||
{isAccountMod && (
|
{isAccountMod && (
|
||||||
<>
|
<>
|
||||||
|
{canEditOwnModPost && (
|
||||||
|
<>
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
<label>
|
||||||
|
[
|
||||||
|
<input
|
||||||
|
aria-label={capitalize(t('edit'))}
|
||||||
|
type='checkbox'
|
||||||
|
onChange={() => setIsContentEditorOpen((isOpen) => !isOpen)}
|
||||||
|
checked={isContentEditorOpen}
|
||||||
|
/>
|
||||||
|
{capitalize(t('edit'))}?]
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{isContentEditorOpen && (
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
className={styles.editTextarea}
|
||||||
|
aria-label={capitalize(t('edit'))}
|
||||||
|
value={publishCommentEditOptions.content || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newContent = e.target.value;
|
||||||
|
setPublishCommentEditOptions((state) => ({ ...state, content: newContent }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className={styles.menuItem}>
|
<div className={styles.menuItem}>
|
||||||
<label>
|
<label>
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user