import { useCallback, useMemo, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { autoUpdate, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; import { Comment, PublishCommentEditOptions, useAccount, usePublishCommentEdit, usePublishCommentModeration, PublishCommentModerationOptions, } from '@plebbit/plebbit-react-hooks'; import styles from './edit-menu.module.css'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import useChallengesStore from '../../stores/use-challenges-store'; import { capitalize } from 'lodash'; import useIsMobile from '../../hooks/use-is-mobile'; import useAuthorPrivileges from '../../hooks/use-author-privileges'; const { addChallenge } = useChallengesStore.getState(); const daysToTimestampInSeconds = (days: number) => { const now = new Date(); now.setDate(now.getDate() + days); return Math.floor(now.getTime() / 1000); }; const timestampToDays = (timestamp: number) => { const now = Math.floor(Date.now() / 1000); return Math.max(1, Math.floor((timestamp - now) / (24 * 60 * 60))); }; const EditMenu = ({ post }: { post: Comment }) => { const { t } = useTranslation(); const isMobile = useIsMobile(); const { author, cid, content, deleted, locked, parentCid, pinned, postCid, reason, removed, spoiler, subplebbitAddress } = post || {}; const authorDisplayName = post?.author?.displayName; const modBanExpiresAt = post?.commentModeration?.author?.banExpiresAt; const purged = post?.commentModeration?.purged ?? false; const [isEditMenuOpen, setIsEditMenuOpen] = useState(false); const [isContentEditorOpen, setIsContentEditorOpen] = useState(false); const account = useAccount(); const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useAuthorPrivileges({ commentAuthorAddress: author?.address, subplebbitAddress, postCid, }); const signer = isAccountCommentAuthor ? account?.signer : null; const latestPostRef = useRef(post); latestPostRef.current = post; const onChallenge = useCallback((...args: any) => addChallenge([...args, latestPostRef.current]), []); const defaultPublishEditOptions = useMemo(() => { return { commentCid: cid, subplebbitAddress, // Author edit properties content: isAccountCommentAuthor ? content : undefined, deleted: isAccountCommentAuthor ? (deleted ?? false) : undefined, spoiler: isAccountCommentAuthor ? (spoiler ?? false) : undefined, // Mod edit properties commentModeration: isAccountMod ? { locked: locked ?? false, pinned: pinned ?? false, removed: removed ?? false, purged: purged ?? false, spoiler: spoiler ?? false, reason, author: modBanExpiresAt ? { banExpiresAt: modBanExpiresAt } : undefined, } : undefined, onChallenge, onChallengeVerification: alertChallengeVerificationFailed, onError: (error: Error) => { console.warn(error); alert('Comment edit failed. ' + error.message); }, }; }, [isAccountMod, isAccountCommentAuthor, cid, content, deleted, locked, pinned, reason, removed, purged, spoiler, subplebbitAddress, modBanExpiresAt, onChallenge]); const [publishCommentEditOptions, setPublishCommentEditOptions] = useState(defaultPublishEditOptions); const authorEditOptions = useMemo( () => ({ commentCid: cid, subplebbitAddress, signer, author: signer?.address === author?.address ? { address: signer?.address, displayName: authorDisplayName } : account?.author, content: publishCommentEditOptions.content, deleted: publishCommentEditOptions.deleted, reason: publishCommentEditOptions.reason, spoiler: publishCommentEditOptions.spoiler, onChallenge, onChallengeVerification: alertChallengeVerificationFailed, onError: (error: Error) => { console.warn(error); alert('Comment edit failed. ' + error.message); }, }), [publishCommentEditOptions, cid, subplebbitAddress, signer, account?.author, author?.address, authorDisplayName, onChallenge], ); const modEditOptions = useMemo( () => ({ commentCid: cid, subplebbitAddress, commentModeration: { locked: parentCid === undefined ? publishCommentEditOptions.commentModeration?.locked : undefined, pinned: publishCommentEditOptions.commentModeration?.pinned, removed: publishCommentEditOptions.commentModeration?.removed, purged: publishCommentEditOptions.commentModeration?.purged, spoiler: publishCommentEditOptions.commentModeration?.spoiler, reason: publishCommentEditOptions.reason, author: publishCommentEditOptions.commentModeration?.author, }, author: account?.author, onChallenge, onChallengeVerification: alertChallengeVerificationFailed, onError: (error: Error) => { console.warn(error); alert('Comment moderation failed. ' + error.message); }, }), [publishCommentEditOptions, cid, subplebbitAddress, account?.author, parentCid, onChallenge], ); const { publishCommentEdit: publishAuthorEdit } = usePublishCommentEdit(authorEditOptions); const { publishCommentModeration } = usePublishCommentModeration(modEditOptions); const [banDuration, setBanDuration] = useState(() => defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1, ); const resetMenuState = () => { setPublishCommentEditOptions(defaultPublishEditOptions); setBanDuration( defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1, ); setIsContentEditorOpen(false); }; const onCheckbox = (e: React.ChangeEvent) => { const { id, checked } = e.target; setPublishCommentEditOptions((state) => { const newState = { ...state }; if (isAccountMod) { if (id === 'banUser') { const banValue = checked ? daysToTimestampInSeconds(banDuration) : undefined; newState.commentModeration = { ...newState.commentModeration, author: banValue ? { banExpiresAt: banValue } : undefined, }; } else { newState.commentModeration = { ...newState.commentModeration, [id]: checked, }; } } if (isAccountCommentAuthor) { newState[id] = checked; } return newState; }); }; const onBanDurationChange = (e: React.ChangeEvent) => { const days = parseInt(e.target.value, 10) || 1; setBanDuration(days); setPublishCommentEditOptions((state) => { // Only update ban expiration if ban is currently enabled (checkbox is checked) const isBanEnabled = state.commentModeration?.author?.banExpiresAt !== undefined; return { ...state, commentModeration: { ...state.commentModeration, author: isBanEnabled ? { banExpiresAt: daysToTimestampInSeconds(days) } : state.commentModeration?.author, }, }; }); }; const onPurgeCheckbox = (e: React.ChangeEvent) => { const { checked } = e.target; // Double confirmation for purge action if (checked) { const firstConfirm = window.confirm(t('purge_confirm')); if (!firstConfirm) { return; } const secondConfirm = window.confirm(t('double_confirm')); if (!secondConfirm) { return; } } setPublishCommentEditOptions((state) => { const newState = { ...state }; if (isAccountMod) { newState.commentModeration = { ...newState.commentModeration, purged: checked, }; } return newState; }); }; const { refs, floatingStyles, context } = useFloating({ placement: 'bottom-start', open: isEditMenuOpen, onOpenChange: setIsEditMenuOpen, middleware: [offset(2), shift()], whileElementsMounted: autoUpdate, }); const click = useClick(context); const dismiss = useDismiss(context); const role = useRole(context); const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]); const headingId = useId(); const _publishCommentEdit = async () => { try { if (isAccountCommentAuthor && isAccountMod) { await publishAuthorEdit(); await publishCommentModeration(); } else if (isAccountCommentAuthor) { await publishAuthorEdit(); } else if (isAccountMod) { await publishCommentModeration(); } } catch (error) { if (error instanceof Error) { console.warn(error); alert(error.message); } } setIsEditMenuOpen(false); }; return ( <> { if (cid && (isAccountCommentAuthor || isAccountMod)) { if (!isEditMenuOpen) { resetMenuState(); setIsEditMenuOpen(true); } else { setIsEditMenuOpen(false); } } else { setIsEditMenuOpen(false); alert(parentCid ? t('cannot_edit_reply') : t('cannot_edit_thread')); } }} checked={isEditMenuOpen} /> {isEditMenuOpen && (isAccountCommentAuthor || isAccountMod) && (
{isAccountCommentAuthor && ( <>
{isContentEditorOpen && (