From f1e6f51d69d17a6fa0a1cd60569f9cef7b48bef1 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:32:37 +0300 Subject: [PATCH] Fix observer not seeing messages sent while they were away from the page --- app/features/chat/chat-client.test.ts | 26 ++++++++++ app/features/chat/chat-client.ts | 5 +- changelog/2026-08-29-observer-chat-history.md | 4 ++ e2e/chat.spec.ts | 51 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 changelog/2026-08-29-observer-chat-history.md diff --git a/app/features/chat/chat-client.test.ts b/app/features/chat/chat-client.test.ts index 05f3ad041..05a3f104a 100644 --- a/app/features/chat/chat-client.test.ts +++ b/app/features/chat/chat-client.test.ts @@ -590,6 +590,32 @@ describe("createChatClient", () => { expect(client.getSnapshot().messagesByRoomId.has(50)).toBe(true); }); + test("reopening an observed room refetches its history", async () => { + const observed = room({ id: 50 }); + const harness = createHarness({ + observedRoom: observed, + messages: [message({ id: 1, roomId: 50 })], + }); + const client = await startedClient(harness); + client.ensureRoomKnown(50); + await flush(); + client.ensureMessagesLoaded(50); + await flush(); + + harness.fetchMessages.mockResolvedValue({ + messages: [ + message({ id: 1, roomId: 50 }), + message({ id: 2, roomId: 50 }), + ], + }); + client.ensureMessagesLoaded(50); + await flush(); + + // nothing was pushed to the observer while the room's page was not open + expect(harness.fetchMessages).toHaveBeenCalledTimes(2); + expect(client.getSnapshot().messagesByRoomId.get(50)).toHaveLength(2); + }); + test("an observed room the user's list later carries is superseded by the list version", async () => { const observed = room({ id: 50 }); const harness = createHarness({ observedRoom: observed }); diff --git a/app/features/chat/chat-client.ts b/app/features/chat/chat-client.ts index 3477c5cd6..6d594d47c 100644 --- a/app/features/chat/chat-client.ts +++ b/app/features/chat/chat-client.ts @@ -67,7 +67,7 @@ export interface ChatClient { refreshRooms: () => Promise; /** Fetches a room's info as an observed room when the user's own room list does not carry it (observer access via a route's `chatRooms`). */ ensureRoomKnown: (roomId: number) => void; - /** Fetches the room's history unless it is already loaded or loading. */ + /** Fetches the room's history unless it is already loaded or loading. An observed room's held history is refetched instead of trusted: messages only reach an observer while the route surfacing the room keeps its subscription. */ ensureMessagesLoaded: (roomId: number) => void; /** Reconnect catch-up: refetches the room list and every loaded history. */ catchUp: () => void; @@ -455,7 +455,8 @@ export function createChatClient(deps: ChatClientDeps): ChatClient { void loadObservedRoom(roomId); }, ensureMessagesLoaded: (roomId) => { - if (messagesByRoomId.has(roomId)) return; + const canHaveMissedMessages = roomById(roomId)?.observed ?? false; + if (messagesByRoomId.has(roomId) && !canHaveMissedMessages) return; void loadMessages(roomId); }, catchUp: () => { diff --git a/changelog/2026-08-29-observer-chat-history.md b/changelog/2026-08-29-observer-chat-history.md new file mode 100644 index 000000000..c4c3c43cd --- /dev/null +++ b/changelog/2026-08-29-observer-chat-history.md @@ -0,0 +1,4 @@ +--- +type: bug +--- +Fix tournament staff not seeing chat messages sent while they were on another page diff --git a/e2e/chat.spec.ts b/e2e/chat.spec.ts index d5e93ce7c..a2b42dc00 100644 --- a/e2e/chat.spec.ts +++ b/e2e/chat.spec.ts @@ -25,6 +25,7 @@ import { MobileNav } from "./pages/layout/mobile-nav"; import { SideNav } from "./pages/layout/side-nav"; import { SendouQLookingPage } from "./pages/sendouq/sendouq-looking-page"; import { SendouQMatchPage } from "./pages/sendouq/sendouq-match-page"; +import { TournamentBracketsPage } from "./pages/tournament/tournament-brackets-page"; import { TournamentMatchPage } from "./pages/tournament/tournament-match-page"; test.describe("Chat", () => { @@ -383,6 +384,56 @@ test.describe("Chat", () => { } }); + test("An observer coming back to a match chat sees what was said while they were away", async ({ + page, + browser, + workerBaseURL, + factories, + }) => { + const { tournament, matchId } = await createInProgressMatch(factories, { + name: "Chat Cup", + friendId: NZAP_TEST_ID, + }); + + const participant = await openSecondUser( + browser, + workerBaseURL, + NZAP_TEST_ID, + ); + try { + await new TournamentMatchPage(participant.page).goto({ + tournamentId: tournament.id, + matchId, + }); + const participantChat = new ChatSidebar(participant.page).chat(); + + // the tournament's author, and so an observer of the match chat + await impersonate(page); + const brackets = new TournamentBracketsPage(page); + await brackets.goto(tournament.id); + + const matchPage = await brackets.openMatch(matchId); + const chat = new ChatSidebar(page); + + await expect(chat.locators.openChats).toHaveCount(1); + + // an observer is only pushed the room's messages while on the page that + // surfaced it, so what is said from here on never reaches their history + await matchPage.backToBracket(); + + await participantChat.send("said while you were on the bracket"); + await expect(participantChat.locators.pendingMessages).toHaveCount(0); + + await brackets.openMatch(matchId); + + await expect( + chat.chat().message("said while you were on the bracket"), + ).toBeVisible(); + } finally { + await participant.close(); + } + }); + test("A closed room is readable by staff and denied to its participants", async ({ page, browser,