import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import type { ChallengeVerification, Comment } from '@bitsocial/bitsocial-react-hooks'; import { useAccount, usePublishCommentEdit } from '@bitsocial/bitsocial-react-hooks'; import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; import styles from './post-menu-mobile.module.css'; import { getCommentMediaInfo } from '../../../lib/utils/media-utils'; import { copyShareLinkToClipboard, isValidURL, type ShareLinkType } from '../../../lib/utils/url-utils'; import { copyToClipboard } from '../../../lib/utils/clipboard-utils'; import { getBoardPath } from '../../../lib/utils/route-utils'; import { useDirectories } from '../../../hooks/use-directories'; import useEditCommentPrivileges from '../../../hooks/use-author-privileges'; import { useBoardPseudonymityMode } from '../../../hooks/use-board-pseudonymity-mode'; import useHide from '../../../hooks/use-hide'; import EditMenu from '../../edit-menu/edit-menu'; import { isPostPageView } from '../../../lib/utils/view-utils'; import { useLocation, useParams } from 'react-router-dom'; import { PostMenuProps } from '../../../lib/utils/post-menu-props'; import { getCommentCommunityAddress, withResolvedCommentCommunityAddress } from '../../../lib/utils/comment-utils'; import { alertChallengeVerificationFailed, type ChallengePublication } from '../../../lib/utils/challenge-utils'; import useChallengesStore from '../../../stores/use-challenges-store'; async function copyShareLinkSafe(boardIdentifier: string, linkType: ShareLinkType, cid?: string): Promise { try { if (linkType === 'thread' && cid) { await copyShareLinkToClipboard(boardIdentifier, linkType, cid); } else { await copyShareLinkToClipboard(boardIdentifier, linkType as Exclude); } } catch (error) { console.error('Failed to copy share link', error); } } async function copyContentIdSafe(cid: string): Promise { try { await copyToClipboard(cid); } catch (error) { console.error('Failed to copy content id', error); } } async function copyUserIdSafe(address: string): Promise { try { await copyToClipboard(address); } catch (error) { console.error('Failed to copy user id', error); } } type HideButtonProps = { cid?: string; comment?: Comment; isReply?: boolean; postCid?: string; onClose?: () => void; }; type CopyLinkButtonProps = | { cid: string; communityAddress: string; linkType: 'thread'; onClose: () => void } | { communityAddress: string; linkType: Exclude; onClose: () => void; cid?: undefined }; const CopyLinkButton = ({ cid, communityAddress, linkType, onClose }: CopyLinkButtonProps) => { const { t } = useTranslation(); const directories = useDirectories(); const boardIdentifier = getBoardPath(communityAddress, directories); const copyShareLink = async () => { await copyShareLinkSafe(boardIdentifier, linkType, linkType === 'thread' ? cid : undefined); onClose(); }; return ( ); }; const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => void }) => { const { t } = useTranslation(); const copyContentId = async () => { await copyContentIdSafe(cid); onClose(); }; return ( ); }; const CopyUserIdButton = ({ address, onClose }: { address: string; onClose: () => void }) => { const { t } = useTranslation(); const copyUserId = async () => { await copyUserIdSafe(address); onClose(); }; return ( ); }; const ImageSearchButtons = ({ url, onClose }: { url: string; onClose: () => void }) => { const { t } = useTranslation(); const encodedUrl = encodeURIComponent(url); return ( ); }; type DeletePostButtonProps = { post: Comment; onClose: () => void; }; const DeletePostButton = ({ post, onClose }: DeletePostButtonProps) => { const { t } = useTranslation(); const account = useAccount(); const resolvedPost = withResolvedCommentCommunityAddress(post); const { author, cid } = resolvedPost || {}; const communityAddress = getCommentCommunityAddress(resolvedPost); const { isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: author?.address || '', communityAddress: communityAddress || '', postCid: resolvedPost?.postCid, }); const signer = isAccountCommentAuthor ? account?.signer : undefined; const signerMatchesAuthor = Boolean(signer?.address && author?.address && signer.address === author.address); const latestPostRef = useRef(resolvedPost); useEffect(() => { latestPostRef.current = resolvedPost; }, [resolvedPost]); const onChallenge = useCallback(async (...args: unknown[]) => { useChallengesStore.getState().addChallenge([...args, latestPostRef.current]); }, []); const deleteOptions = useMemo( () => ({ commentCid: cid, communityAddress, deleted: true, ...(isAccountCommentAuthor && signer ? { signer, author: signerMatchesAuthor ? { address: signer.address, displayName: resolvedPost?.author?.displayName } : account?.author, } : {}), onChallenge, onChallengeVerification: async (challengeVerification: ChallengeVerification, publication: ChallengePublication | undefined) => { alertChallengeVerificationFailed(challengeVerification, publication); }, onError: (error: Error) => { console.warn(error); alert('Comment edit failed. ' + error.message); }, }), [cid, communityAddress, isAccountCommentAuthor, signer, signerMatchesAuthor, resolvedPost?.author?.displayName, account?.author, onChallenge], ); const { publishCommentEdit } = usePublishCommentEdit(deleteOptions); const deletePost = async () => { const confirmed = window.confirm(t('delete_post_confirm')); if (!confirmed) { return; } try { await publishCommentEdit(); onClose(); } catch (error) { if (error instanceof Error) { alert(error.message); } } }; return ( ); }; const HidePostButton = ({ cid, comment, isReply, onClose, postCid }: HideButtonProps) => { const { t } = useTranslation(); const { hide, hidden, unhide } = useHide({ cid: cid || '', comment }); const isInPostView = isPostPageView(useLocation().pathname, useParams()); const togglePostHidden = () => { if (hidden) { unhide(); } else { hide(); } onClose?.(); }; return ( (!isInPostView || isReply) && ( ) ); }; type PostMenuMobileProps = { postMenu: PostMenuProps; editMenuPost?: Comment; }; const PostMenuMobile = ({ postMenu, editMenuPost }: PostMenuMobileProps) => { const { authorAddress, cid, communityAddress, deleted, link, linkHeight, linkWidth, parentCid, postCid, removed, thumbnailUrl } = postMenu || {}; const { isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: authorAddress || '', communityAddress: communityAddress || '', }); const pseudonymityMode = useBoardPseudonymityMode(communityAddress); const canAttemptAuthorDelete = pseudonymityMode !== undefined && pseudonymityMode !== 'none'; const commentMediaInfo = getCommentMediaInfo(link || '', thumbnailUrl || '', linkWidth || 0, linkHeight || 0); const { thumbnail, type, url } = commentMediaInfo || {}; const [isMenuOpen, setIsMenuOpen] = useState(false); const { refs, floatingStyles, context } = useFloating({ placement: 'bottom-start', open: isMenuOpen, onOpenChange: setIsMenuOpen, middleware: [offset({ mainAxis: 3, crossAxis: 8 }), flip(), shift({ padding: 10 })], 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 handleMenuClick = () => { if (cid) { setIsMenuOpen((prev) => !prev); } }; const handleClose = () => setIsMenuOpen(false); return ( <> {!(deleted || removed) && ( <> {isMenuOpen && cid && createPortal(
{cid && communityAddress && } {(isAccountCommentAuthor || canAttemptAuthorDelete) && cid && editMenuPost && } {cid && communityAddress && } {cid && } {authorAddress && } {link && isValidURL(link) && (type === 'image' || type === 'gif' || thumbnail) && url && }
, document.body, )} )} {isAccountMod && cid && editMenuPost && ( )} ); }; export default memo(PostMenuMobile);