mirror of
https://github.com/block/buzz.git
synced 2026-11-05 17:01:35 +00:00
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:
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user