From ddf982332ccae1fb71344304de4b3bdcbe46faa6 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Tue, 3 Nov 2026 22:57:36 -0800 Subject: [PATCH 1/2] fix(messages): pick the latest message with the shared ordering The newest-message lookup behind Up-to-edit and react-to-latest compared whole-second createdAt and kept the first row on a tie, so in a same-second burst it picked an earlier message. Use compareMessages, the rendered order (send ms, then id). Co-authored-by: Claude Opus 5.5 Signed-off-by: Cheng Peng Co-authored-by: Cursor --- .../messages/use-message-selection.test.tsx | 52 +++++++++++++++++++ .../messages/use-message-selection.ts | 3 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/features/messages/use-message-selection.test.tsx diff --git a/src/features/messages/use-message-selection.test.tsx b/src/features/messages/use-message-selection.test.tsx new file mode 100644 index 0000000000..c8b99a1ea5 --- /dev/null +++ b/src/features/messages/use-message-selection.test.tsx @@ -0,0 +1,52 @@ +// @vitest-environment jsdom +import { cleanup, render } from "@testing-library/react"; +import { useRef } from "react"; +import { afterEach, expect, it } from "vitest"; +import type { ChannelMessage } from "../relay/contracts"; +import type { RelaySession } from "../relay/session"; +import { selectNewestMessage } from "./message-selection"; +import { useMessageSelection } from "./use-message-selection"; + +afterEach(cleanup); + +const message = (id: string, createdAt: number, createdAtMs?: number) => + ({ id, createdAt, createdAtMs }) as ChannelMessage; + +function List({ rows }: { rows: readonly ChannelMessage[] }) { + const list = useRef(null); + useMessageSelection(list, rows, {} as RelaySession); + return ( +
+ {rows.map((row) => ( +
+
{row.id}
+
+ ))} +
+ ); +} + +function newestSelected(rows: readonly ChannelMessage[]) { + const { getByTestId } = render(); + expect(selectNewestMessage(getByTestId("list"))).toBe(true); + return document.activeElement?.closest("[data-message-id]") + ?.dataset.messageId; +} + +it("selects the latest message of a same-second burst by send ms, then id", () => { + expect( + newestSelected([message("z", 100, 100_100), message("a", 100, 100_200)]), + ).toBe("a"); + cleanup(); + expect(newestSelected([message("a", 100), message("b", 100)])).toBe("b"); +}); + +it("selects the latest message across seconds and skips membership notices", () => { + const notice = { + ...message("notice", 300), + membership: {}, + } as ChannelMessage; + expect( + newestSelected([message("old", 100), message("new", 200), notice]), + ).toBe("new"); +}); diff --git a/src/features/messages/use-message-selection.ts b/src/features/messages/use-message-selection.ts index f39a4ed060..b0bb1a1476 100644 --- a/src/features/messages/use-message-selection.ts +++ b/src/features/messages/use-message-selection.ts @@ -1,5 +1,6 @@ import { useLayoutEffect, useRef, type RefObject } from "react"; import type { ChannelMessage } from "../relay/contracts"; +import { compareMessages } from "../relay/message-order"; import type { RelaySession } from "../relay/session"; import { useMessageEditScope } from "./MessageEditScope"; import { useMessageKeyActions } from "./MessageManagement"; @@ -26,7 +27,7 @@ export function useMessageSelection( newest: () => latest.current.reduce( (newest, row) => - !row.membership && (!newest || row.createdAt > newest.createdAt) + !row.membership && (!newest || compareMessages(row, newest) > 0) ? row : newest, undefined, From 3695aeec0b51cde781ecf8fcc8383424d959c156 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Tue, 3 Nov 2026 23:19:28 -0800 Subject: [PATCH 2/2] test(messages): mount the newest-message test in StrictMode Follow the mounted-test conventions in docs/contributing.md: render with reactStrictMode and find the list by its region role instead of a test id. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src/features/messages/use-message-selection.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/messages/use-message-selection.test.tsx b/src/features/messages/use-message-selection.test.tsx index c8b99a1ea5..b9dc681666 100644 --- a/src/features/messages/use-message-selection.test.tsx +++ b/src/features/messages/use-message-selection.test.tsx @@ -16,7 +16,7 @@ function List({ rows }: { rows: readonly ChannelMessage[] }) { const list = useRef(null); useMessageSelection(list, rows, {} as RelaySession); return ( -
+
{rows.map((row) => (
{row.id}
@@ -27,8 +27,10 @@ function List({ rows }: { rows: readonly ChannelMessage[] }) { } function newestSelected(rows: readonly ChannelMessage[]) { - const { getByTestId } = render(); - expect(selectNewestMessage(getByTestId("list"))).toBe(true); + const { getByRole } = render(, { reactStrictMode: true }); + expect(selectNewestMessage(getByRole("region", { name: "Messages" }))).toBe( + true, + ); return document.activeElement?.closest("[data-message-id]") ?.dataset.messageId; }