fix(memo): preserve expanded todo list state
This commit is contained in:
parent
8f1377324f
commit
ecbe2ab797
4 changed files with 102 additions and 19 deletions
|
|
@ -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<HTMLDivElement>(null);
|
||||
const [mode, setMode] = useState<ContentCompactView | undefined>(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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MemoBodyProps> = ({ 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<MemoBodyProps> = ({ compact }) => {
|
|||
)}
|
||||
>
|
||||
<MemoContent
|
||||
key={`${memo.name}-${contentRevision}`}
|
||||
key={memo.name}
|
||||
content={memo.content}
|
||||
onClick={handleMemoContentClick}
|
||||
onDoubleClick={handleMemoContentDoubleClick}
|
||||
|
|
|
|||
86
web/tests/memo-body-compact.test.tsx
Normal file
86
web/tests/memo-body-compact.test.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import MemoBody from "@/components/MemoView/components/MemoBody";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
memo: {
|
||||
name: "memos/1",
|
||||
content: "",
|
||||
relations: [],
|
||||
attachments: [],
|
||||
reactions: [],
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/i18n", () => ({
|
||||
useTranslate: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/MemoContent/MentionResolutionContext", () => ({
|
||||
useResolvedMentionUsernames: () => new Set<string>(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/MemoContent/MemoMarkdownRenderer", () => ({
|
||||
MemoMarkdownRenderer: ({ content }: { content: string }) => <div>{content}</div>,
|
||||
}));
|
||||
|
||||
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("<MemoBody /> 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(<MemoBody compact={true} />);
|
||||
|
||||
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(<MemoBody compact={true} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /memo\.show-less/ })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue