fix(memo): preserve expanded todo list state

pull/6028/head
boojack 1 month ago
parent 8f1377324f
commit ecbe2ab797

@ -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}

@ -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…
Cancel
Save