import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { Comment, setAccount, useAccount, useAccountComment, useEditedComment } from '@bitsocial/bitsocial-react-hooks'; import getShortAddress from '../../lib/get-short-address'; import { communitiesPagesStore as useCommunitiesPagesStore } from '../../lib/bitsocial-internals/stores'; import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils'; import { getExpiringMediaLinkAlert, getPublishFileDisplayName, getPublishLinkOptions, isPublishFileMediaLink, isPublishFileMediaType, } from '../../lib/utils/media-link-validation-utils'; import { type DiceRoll, type FortuneEntry, type PostOptionsValidationError, POST_OPTIONS_VALIDATION_DELAY_MS, getContentWithPostOptionState as getContentWithOptions, getNonokoPendingRouteState, getPostOptionsDirectoryCode, getPostOptionsPublishContentLength, getPostOptionsValidationError, hasNonokoOption, isPostOptionsValidationError, } from '../../lib/utils/post-options-utils'; import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils'; import { isValidPublishURL, isValidURL } from '../../lib/utils/url-utils'; import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils'; import { hasModQueueAccessRole } from '../../lib/utils/mod-access'; import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils'; import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory, type CommentFlagSelectOption } from '../../lib/comment-flag-selection'; import { FLASH_TAG_OPTIONS, getFlashTagPublishOptionsForDirectoryCode, isFlashDirectoryCode, type FlashTagOption } from '../../lib/flash-tags'; import { isMathDirectoryCode } from '../../lib/math-tags'; import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses'; import { useDirectories } from '../../hooks/use-directories'; import { useDirectoryEntry } from '../../hooks/use-directory-entry'; import { useCommunityField } from '../../hooks/use-stable-community'; import useIsMobile from '../../hooks/use-is-mobile'; import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address'; import { normalizeAccountCommentIndex } from '../../lib/utils/account-comment-index-utils'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import { useYouTubeThumbnailLinkConversion } from '../../hooks/use-youtube-thumbnail-link-conversion'; import usePublishSubmissionGuard from '../../hooks/use-publish-submission-guard'; import usePublishPost from '../../hooks/use-publish-post'; import usePublishReply from '../../hooks/use-publish-reply'; import { useFileUpload } from '../../hooks/use-file-upload'; import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls'; import { OEKAKI_WEB_WARNING_TEXT } from '../../lib/oekaki/oekaki-copy'; import { isCommentArchived } from '../../lib/utils/comment-moderation-utils'; import useMediaHostingStore from '../../stores/use-media-hosting-store'; import BoardOfflineAlert from '../board-offline-alert/board-offline-alert'; import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar'; import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis'; import OekakiDrawingControls from '../oekaki-drawing-controls/oekaki-drawing-controls'; import TexLogo from '../tex-logo/tex-logo'; import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message'; import styles from './post-form.module.css'; import capitalize from 'lodash/capitalize'; import debounce from 'lodash/debounce'; const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg'; const POST_FORM_FILE_DISPLAY_MAX_LENGTH = 28; const mergeFlairs = (...flairGroups: Array): Comment['flairs'] | undefined => { const flairs = flairGroups.flatMap((group) => (Array.isArray(group) ? group : [])); return flairs.length > 0 ? flairs : undefined; }; const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | null | undefined, noFileLabel: string, requireFile: boolean): string => { const raw = getPublishFileDisplayName(url, uploadedFileName, requireFile); if (!raw) return noFileLabel; return truncateWithEllipsisInMiddle(raw, POST_FORM_FILE_DISPLAY_MAX_LENGTH); }; export const LinkTypePreviewer = ({ link, requireFile = false }: { link: string; requireFile?: boolean }) => { const { t } = useTranslation(); const mediaInfo = getLinkMediaInfo(link); let type = mediaInfo?.type; const { status: gifFrameStatus } = useFetchGifFirstFrame(type === 'gif' ? mediaInfo?.url : undefined); if (requireFile && isValidURL(link) && !isPublishFileMediaType(type)) { return {t('not_a_file')}; } if (type === 'gif' && gifFrameStatus === 'ready') { type = t('animated_gif'); } else if (type === 'gif') { type = t('gif'); } else if (type) { type = getDisplayMediaInfoType(type, t); } return isValidURL(link) ? `${t('file')}: ${type}` : t('invalid_url'); }; const PostFormActions = ({ disableReplyPublish = false, variant, t, isInPostView, onPublishReply, onPublishPost, handleUpload, isPublishSubmissionInFlight, isUploading, showUploadControls, }: { disableReplyPublish?: boolean; variant: 'reply' | 'post' | 'upload'; t: TFunction; isInPostView: boolean; onPublishReply: () => void | Promise; onPublishPost: () => void | Promise; handleUpload: () => void; isPublishSubmissionInFlight: boolean; isUploading: boolean; showUploadControls: boolean; }) => { if (variant === 'reply' && isInPostView) { return ( ); } if (variant === 'post' && !isInPostView) { return ( ); } if (variant === 'upload' && showUploadControls) { return ( ); } return null; }; interface PostFormFieldsProps { t: TFunction; account: ReturnType; displayName: string | undefined; bbcodePreviewContent: string; isInPostView: boolean; isBbcodePreviewing: boolean; postCid: string; subjectRef: React.Ref; optionsRef: React.RefObject; flagRef: React.RefObject; flashTagRef: React.RefObject; textRef: React.RefObject; urlRef: React.Ref; url: string; handleContentChange: (e: React.ChangeEvent) => void; handleContentValueChange: (content: string, options?: string) => void; handleLinkChange: (link: string) => void; handleLinkBlur: () => void; handleOptionsChange: (e: React.ChangeEvent) => void; disableLinkInput: boolean; setPublishPostOptions: (opts: Record) => void; setPublishReplyOptions: (opts: Record) => void; isUploading: boolean; uploadedFileName: string | null | undefined; showUploadControls: boolean; showOekakiControls: boolean; showSpoilerForPost: boolean; showSpoilerForReply: boolean; isInAllView: boolean; isInSubscriptionsView: boolean; isInModView: boolean; directories: ReturnType; accountCommunityAddresses: string[]; subscriptions: string[]; communityAddress: string | undefined; rulesPath: string; requirePostLinkIsMedia: boolean; flagOptions: CommentFlagSelectOption[]; flashTagOptions: FlashTagOption[]; showFlashTagSelector: boolean; showFlashUploadPrompt: boolean; showMathTagsPrompt: boolean; showBbcodeToolbar: boolean; onBbcodePreviewToggle: () => void; onPublishReply: () => void | Promise; onPublishPost: () => void | Promise; handleUpload: () => void; uploadFile: ReturnType['uploadFile']; onOekakiClearUploadedUrl: (url: string) => void; isPublishSubmissionInFlight: boolean; disableReplyPublish: boolean; } const PostFormFields = ({ t, account, displayName, bbcodePreviewContent, isInPostView, isBbcodePreviewing, postCid, subjectRef, optionsRef, flagRef, flashTagRef, textRef, urlRef, url, handleContentChange, handleContentValueChange, handleLinkChange, handleLinkBlur, handleOptionsChange, disableLinkInput, setPublishPostOptions, setPublishReplyOptions, isUploading, uploadedFileName, showUploadControls, showOekakiControls, showSpoilerForPost, showSpoilerForReply, isInAllView, isInSubscriptionsView, isInModView, directories, accountCommunityAddresses, subscriptions, communityAddress, rulesPath, requirePostLinkIsMedia, flagOptions, flashTagOptions, showFlashTagSelector, showFlashUploadPrompt, showMathTagsPrompt, showBbcodeToolbar, onBbcodePreviewToggle, onPublishReply, onPublishPost, handleUpload, uploadFile, onOekakiClearUploadedUrl, isPublishSubmissionInFlight, disableReplyPublish, }: PostFormFieldsProps) => ( <> {t('name')} { const newDisplayName = e.target.value.trim() || undefined; setAccount({ ...account, author: { ...account?.author, displayName: newDisplayName } }); if (isInPostView) { setPublishReplyOptions({ displayName: newDisplayName }); } else { setPublishPostOptions({ displayName: newDisplayName }); } }} /> {t('options')} {!isInPostView && ( {t('subject')} { setPublishPostOptions({ title: e.target.value }); }} /> )} {showBbcodeToolbar ? ( format handleContentValueChange(content)} isPreviewing={isBbcodePreviewing} onPreviewToggle={onBbcodePreviewToggle} /> ) : null} {t('comment')} {showBbcodeToolbar && isBbcodePreviewing && ( )}