import { type ComponentType, forwardRef, memo, Suspense, useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { useLocation } from "react-router-dom"; import { useResolvedUser } from "@/components/MemoContent/MentionResolutionContext"; import { loadMemoEditor } from "@/components/MemoEditor/loader"; import type { MemoEditorProps } from "@/components/MemoEditor/types"; import { useAuth } from "@/contexts/AuthContext"; import useCurrentUser from "@/hooks/useCurrentUser"; import { isMemoBlurred } from "@/lib/tag"; import { cn } from "@/lib/utils"; import { State } from "@/types/proto/api/v1/common_pb"; import { lazyWithReload } from "@/utils/lazy"; import { isSuperUser } from "@/utils/user"; import { MemoBody, MemoCommentListView, MemoHeader } from "./components"; import { MEMO_CARD_BASE_CLASSES } from "./constants"; import { useImagePreview } from "./hooks"; import { computeCommentAmount, MemoViewContext } from "./MemoViewContext"; import { isMemoDetailPath, resolveMemoOrigin } from "./navigation"; import type { MemoViewHandle, MemoViewProps } from "./types"; const MemoShareImageDialog = lazyWithReload(() => import("../MemoActionMenu/MemoShareImageDialog")); const PreviewImageDialog = lazyWithReload(() => import("../PreviewImageDialog")); const MemoView = forwardRef((props, ref) => { const { memo: memoData, className, parentPage: parentPageProp, parentScope: parentScopeProp, compact, timeDisplay, showCreator, showVisibility, showPinned, showSpace, } = props; const cardRef = useRef(null); const [showEditor, setShowEditor] = useState(false); const [EditorComponent, setEditorComponent] = useState>(); const [cardWidth, setCardWidth] = useState(0); const currentUser = useCurrentUser(); const { userTagsSetting } = useAuth(); const creator = useResolvedUser(memoData.creator, { enabled: Boolean(showCreator || props.shareImageDialogOpen) }); const isArchived = memoData.state === State.ARCHIVED; const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser); const location = useLocation(); const { parentPage, parentScope } = resolveMemoOrigin({ explicitParentPage: parentPageProp, explicitParentScope: parentScopeProp, pathname: location.pathname, search: location.search, memoName: memoData.name, }); // Blur content when any tag has blur_content enabled in the current user's tag settings. const [showBlurredContent, setShowBlurredContent] = useState(false); const blurred = isMemoBlurred(memoData, userTagsSetting); const toggleBlurVisibility = useCallback(() => setShowBlurredContent((prev) => !prev), []); const { previewState, openPreview, setPreviewOpen } = useImagePreview(); const editorHostRef = useRef(null); const focusMountedEditor = useCallback(() => { const codeMirrorContent = editorHostRef.current?.querySelector('.cm-content[contenteditable="true"]'); const fallbackInput = editorHostRef.current?.querySelector("textarea, input"); (codeMirrorContent ?? fallbackInput)?.focus(); }, []); const openEditor = useCallback(() => { if (showEditor && EditorComponent) { focusMountedEditor(); return; } void loadMemoEditor() .then(({ default: MemoEditor }) => { setEditorComponent(() => MemoEditor); setShowEditor(true); }) .catch(() => undefined); }, [EditorComponent, focusMountedEditor, showEditor]); const closeEditor = useCallback(() => setShowEditor(false), []); useImperativeHandle(ref, () => ({ openEditor }), [openEditor]); const isInMemoDetailPage = isMemoDetailPath(location.pathname, memoData.name); const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0; // The card width is only needed by the share-image dialog. Keep feed cards // free of a permanent ResizeObserver and measure only while that dialog is open. useLayoutEffect(() => { if (!props.shareImageDialogOpen) { return; } const card = cardRef.current; if (!card) { return; } const updateWidth = (nextWidth?: number) => { const width = Math.round(nextWidth ?? card.getBoundingClientRect().width); setCardWidth((prev) => (prev === width ? prev : width)); }; updateWidth(); if (typeof ResizeObserver === "undefined") { const handleResize = () => updateWidth(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); } const resizeObserver = new ResizeObserver((entries) => { updateWidth(entries[0]?.contentRect.width); }); resizeObserver.observe(card); return () => resizeObserver.disconnect(); }, [props.shareImageDialogOpen]); const contextValue = useMemo( () => ({ memo: memoData, creator, currentUser, parentPage, parentScope, cardWidth, isArchived, readonly, showBlurredContent, blurred, openEditor, toggleBlurVisibility, openPreview, }), [ memoData, creator, currentUser, parentPage, parentScope, cardWidth, isArchived, readonly, showBlurredContent, blurred, openEditor, toggleBlurVisibility, openPreview, ], ); const article = (
{previewState.items.length > 0 && ( )} {props.onShareImageDialogOpenChange && props.shareImageDialogOpen && ( )}
); const memoDisplay = showCommentPreview ? (
{article}
) : ( article ); return ( {showEditor && EditorComponent ? (
) : ( memoDisplay )}
); }); MemoView.displayName = "MemoView"; export default memo(MemoView);