From ecbe2ab7977fcd3521aeae1226e816a9dc2a6a40 Mon Sep 17 00:00:00 2001 From: boojack Date: Mon, 8 Jun 2026 19:11:29 +0800 Subject: [PATCH] fix(memo): preserve expanded todo list state --- web/src/components/MemoContent/hooks.ts | 20 +++-- web/src/components/MemoContent/index.tsx | 2 +- .../MemoView/components/MemoBody.tsx | 13 +-- web/tests/memo-body-compact.test.tsx | 86 +++++++++++++++++++ 4 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 web/tests/memo-body-compact.test.tsx diff --git a/web/src/components/MemoContent/hooks.ts b/web/src/components/MemoContent/hooks.ts index bc8187472..8ca09836f 100644 --- a/web/src/components/MemoContent/hooks.ts +++ b/web/src/components/MemoContent/hooks.ts @@ -2,17 +2,25 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { COMPACT_STATES, getMaxDisplayHeight } from "./constants"; import type { ContentCompactView } from "./types"; -export const useCompactMode = (enabled: boolean) => { +export const useCompactMode = (enabled: boolean, revision: string) => { const containerRef = useRef(null); const [mode, setMode] = useState(undefined); useEffect(() => { - if (!enabled || !containerRef.current) return; - const maxHeight = getMaxDisplayHeight(); - if (containerRef.current.getBoundingClientRect().height > maxHeight) { - setMode("ALL"); + if (!enabled || !containerRef.current) { + setMode(undefined); + return; } - }, [enabled]); + + const maxHeight = getMaxDisplayHeight(); + const shouldCompact = Math.max(containerRef.current.scrollHeight, containerRef.current.getBoundingClientRect().height) > maxHeight; + setMode((currentMode) => { + if (!shouldCompact) { + return undefined; + } + return currentMode ?? "ALL"; + }); + }, [enabled, revision]); const toggle = useCallback(() => { if (!mode) return; diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index 67fa19953..069f0c183 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -16,7 +16,7 @@ const MemoContent = (props: MemoContentProps) => { containerRef: memoContentContainerRef, mode: showCompactMode, toggle: toggleCompactMode, - } = useCompactMode(Boolean(props.compact)); + } = useCompactMode(Boolean(props.compact), content); const mentionUsernames = useMemo(() => extractMentionUsernames(content), [content]); const resolvedMentionUsernames = useResolvedMentionUsernames(mentionUsernames); diff --git a/web/src/components/MemoView/components/MemoBody.tsx b/web/src/components/MemoView/components/MemoBody.tsx index f760e35a5..a3142240f 100644 --- a/web/src/components/MemoView/components/MemoBody.tsx +++ b/web/src/components/MemoView/components/MemoBody.tsx @@ -1,4 +1,3 @@ -import { useMemo } from "react"; import { AttachmentListView, LocationDisplayView, RelationListView } from "@/components/MemoMetadata"; import { cn } from "@/lib/utils"; import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; @@ -23,22 +22,12 @@ const BlurOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => { ); }; -const getContentRevision = (content: string) => { - let hash = 2166136261; - for (let i = 0; i < content.length; i++) { - hash ^= content.charCodeAt(i); - hash = Math.imul(hash, 16777619); - } - return `${content.length}-${hash >>> 0}`; -}; - const MemoBody: React.FC = ({ compact }) => { const { memo, parentPage, showBlurredContent, blurred, readonly, openEditor, openPreview, toggleBlurVisibility } = useMemoViewContext(); const { handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({ readonly, openEditor, openPreview }); const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); - const contentRevision = useMemo(() => getContentRevision(memo.content), [memo.content]); return ( <> @@ -49,7 +38,7 @@ const MemoBody: React.FC = ({ compact }) => { )} > ({ + memo: { + name: "memos/1", + content: "", + relations: [], + attachments: [], + reactions: [], + }, +})); + +vi.mock("@/utils/i18n", () => ({ + useTranslate: () => (key: string) => key, +})); + +vi.mock("@/components/MemoContent/MentionResolutionContext", () => ({ + useResolvedMentionUsernames: () => new Set(), +})); + +vi.mock("@/components/MemoContent/MemoMarkdownRenderer", () => ({ + MemoMarkdownRenderer: ({ content }: { content: string }) =>
{content}
, +})); + +vi.mock("@/components/MemoMetadata", () => ({ + AttachmentListView: () => null, + LocationDisplayView: () => null, + RelationListView: () => null, +})); + +vi.mock("@/components/MemoReactionListView", () => ({ + MemoReactionListView: () => null, +})); + +vi.mock("@/components/MemoView/hooks", () => ({ + useMemoHandlers: () => ({ + handleMemoContentClick: vi.fn(), + handleMemoContentDoubleClick: vi.fn(), + }), +})); + +vi.mock("@/components/MemoView/MemoViewContext", () => ({ + useMemoViewContext: () => ({ + memo: mockState.memo, + parentPage: "", + showBlurredContent: false, + blurred: false, + readonly: false, + openEditor: vi.fn(), + openPreview: vi.fn(), + toggleBlurVisibility: vi.fn(), + }), +})); + +const createMemo = (content: string) => ({ + name: "memos/1", + content, + relations: [], + attachments: [], + reactions: [], +}); + +describe(" compact content", () => { + it("keeps expanded compact content expanded when memo content changes", async () => { + vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function () { + if ((this as HTMLElement).hasAttribute("data-memo-content")) { + return { x: 0, y: 0, width: 320, height: 1000, top: 0, right: 320, bottom: 1000, left: 0, toJSON: () => ({}) }; + } + return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0, toJSON: () => ({}) }; + }); + + mockState.memo = createMemo("line 1\nline 2"); + const { rerender } = render(); + + await waitFor(() => expect(screen.getByRole("button", { name: /memo\.show-more/ })).toBeInTheDocument()); + fireEvent.click(screen.getByRole("button", { name: /memo\.show-more/ })); + expect(screen.getByRole("button", { name: /memo\.show-less/ })).toBeInTheDocument(); + + mockState.memo = createMemo("line 1\nline 2 updated"); + rerender(); + + expect(screen.getByRole("button", { name: /memo\.show-less/ })).toBeInTheDocument(); + }); +});