feat(desktop): seed huddle thread replies

Render reply events already present in the bounded channel window while the
complete thread query loads. Keep the window data as placeholder-only so it
does not delay the authoritative request.

Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
This commit is contained in:
Wren
2026-08-22 17:40:09 -04:00
committed by Meli
parent 9d139889d0
commit 1165f228e9
3 changed files with 166 additions and 2 deletions

View File

@@ -4,8 +4,10 @@ import { huddleWindowChannelId } from "@/features/huddle/lib/huddleWindow";
import { mergeMessages } from "@/features/messages/hooks";
import {
channelWindowThreadSummaries,
flattenChannelWindowEvents,
type ChannelWindowStore,
} from "@/features/messages/lib/channelWindowStore";
import { getThreadReference } from "@/features/messages/lib/threading";
import { useThreadRepliesForRoots } from "@/features/messages/useThreadReplies";
import type { Channel, RelayEvent } from "@/shared/api/types";
@@ -25,6 +27,19 @@ type HuddleChannelMessagesOptions = {
windowStore?: ChannelWindowStore;
};
export function seedHuddleThreadReplies(
windowStore: ChannelWindowStore | undefined,
): ReadonlyMap<string, RelayEvent[]> {
const repliesByRoot = new Map<string, RelayEvent[]>();
if (!windowStore) return repliesByRoot;
for (const event of flattenChannelWindowEvents(windowStore)) {
const { parentId, rootId } = getThreadReference(event.tags);
if (!parentId || !rootId) continue;
repliesByRoot.set(rootId, [...(repliesByRoot.get(rootId) ?? []), event]);
}
return repliesByRoot;
}
export function useHuddleChannelMessages({
activeChannel,
isHuddleTranscript,
@@ -51,9 +66,16 @@ export function useHuddleChannelMessages({
: [],
[isHuddleTranscript, threadSummaries],
);
const placeholderThreadRepliesByRoot = React.useMemo(
() => seedHuddleThreadReplies(windowStore),
[windowStore],
);
const huddleThreadReplies = useThreadRepliesForRoots(
activeChannel,
huddleThreadRootIds,
{
placeholderDataByRoot: placeholderThreadRepliesByRoot,
},
);
const resolvedMessages = React.useMemo(
() =>

View File

@@ -1,7 +1,15 @@
import assert from "node:assert/strict";
import test from "node:test";
import { collectThreadAuxMessageIds } from "./useThreadReplies.ts";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { JSDOM } from "jsdom";
import React from "react";
import { relayClient } from "../../shared/api/relayClient.ts";
import {
collectThreadAuxMessageIds,
useThreadRepliesForRoots,
} from "./useThreadReplies.ts";
const ROOT_ID = "1".repeat(64);
const REPLY_ID = "2".repeat(64);
@@ -28,3 +36,131 @@ test("thread aux hydration includes and deduplicates root and reply ids", () =>
[ROOT_ID, REPLY_ID],
);
});
async function withHookEnvironment(run) {
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
url: "http://localhost/",
});
const previousGlobals = {
window: globalThis.window,
document: globalThis.document,
navigator: globalThis.navigator,
tauri: globalThis.__TAURI_INTERNALS__,
act: globalThis.IS_REACT_ACT_ENVIRONMENT,
};
const pendingRequests = new Map();
const tauriInternals = {
invoke: (command, args) => {
assert.equal(command, "get_thread_replies");
return new Promise((resolve) => {
pendingRequests.set(args.rootEventId, resolve);
});
},
transformCallback: () => 1,
};
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
__TAURI_INTERNALS__: tauriInternals,
IS_REACT_ACT_ENVIRONMENT: true,
});
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: dom.window.navigator,
});
dom.window.__TAURI_INTERNALS__ = tauriInternals;
try {
const { act, renderHook } = await import("@testing-library/react");
await run({ act, pendingRequests, renderHook });
} finally {
dom.window.close();
Object.assign(globalThis, {
window: previousGlobals.window,
document: previousGlobals.document,
__TAURI_INTERNALS__: previousGlobals.tauri,
IS_REACT_ACT_ENVIRONMENT: previousGlobals.act,
});
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: previousGlobals.navigator,
});
}
}
function hookWrapper(client) {
return ({ children }) =>
React.createElement(QueryClientProvider, { client }, children);
}
const channel = { id: "huddle", channelType: "stream" };
test("window-seeded huddle roots fetch and settle without a visible gap", async () => {
const auxCalls = [];
const fetchAuxEventsByReference = relayClient.fetchAuxEventsByReference;
relayClient.fetchAuxEventsByReference = async (
channelId,
referencedEventIds,
buildFilter,
) => {
auxCalls.push({
channelId,
referencedEventIds,
filter: buildFilter(channelId, referencedEventIds),
});
return [];
};
try {
await withHookEnvironment(async ({ act, pendingRequests, renderHook }) => {
const seededRootId = "3".repeat(64);
const seededReply = reply("4".repeat(64));
seededReply.tags = [["e", seededRootId, "", "reply"]];
const authoritativeReply = reply("5".repeat(64));
authoritativeReply.tags = [["e", seededRootId, "", "reply"]];
const client = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
});
const placeholderDataByRoot = new Map([[seededRootId, [seededReply]]]);
const view = renderHook(
() =>
useThreadRepliesForRoots(channel, [seededRootId], {
placeholderDataByRoot,
}),
{ wrapper: hookWrapper(client) },
);
try {
assert.deepEqual(view.result.current.events, [seededReply]);
assert.equal(pendingRequests.has(seededRootId), true);
await act(async () => {
pendingRequests.get(seededRootId)({
events: [authoritativeReply],
nextCursor: null,
});
await new Promise((resolve) => setTimeout(resolve, 0));
});
assert.deepEqual(view.result.current.events, [authoritativeReply]);
assert.equal(auxCalls.length, 2);
for (const call of auxCalls) {
assert.equal(call.channelId, channel.id);
assert.deepEqual(call.referencedEventIds, [
seededRootId,
authoritativeReply.id,
]);
assert.deepEqual(call.filter["#e"], [
seededRootId,
authoritativeReply.id,
]);
}
} finally {
view.unmount();
await client.cancelQueries();
client.clear();
client.unmount();
}
});
} finally {
relayClient.fetchAuxEventsByReference = fetchAuxEventsByReference;
}
});

View File

@@ -4,7 +4,6 @@ import {
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
collectMessageIdsForAuxBackfill,
fetchStructuralAuxForMessages,
@@ -134,17 +133,24 @@ export function useThreadReplies(
* replies into the chat timeline so companion and in-app presentations show the
* same conversation without opening a transient thread surface.
*/
export type ThreadRepliesForRootsOptions = {
placeholderDataByRoot?: ReadonlyMap<string, RelayEvent[]>;
};
export function useThreadRepliesForRoots(
activeChannel: Channel | null,
rootIds: readonly string[],
options: ThreadRepliesForRootsOptions = {},
) {
const queryClient = useQueryClient();
const channelId = activeChannel?.id ?? "none";
const placeholderDataByRoot = options.placeholderDataByRoot;
return useQueries({
queries: rootIds.map((rootId) => ({
queryKey: threadRepliesKey(channelId, rootId),
enabled: activeChannel !== null && activeChannel.channelType !== "forum",
queryFn: () => loadThreadReplies(queryClient, channelId, rootId),
placeholderData: () => placeholderDataByRoot?.get(rootId),
staleTime: 0,
gcTime: 60 * 60 * 1_000,
})),