mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): backfill edits for thread replies so edited replies survive refetch (#1610)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
collectAuxEventIdsForDeletionBackfill,
|
||||
collectMessageIdsForAuxBackfill,
|
||||
fetchStructuralAuxForMessages,
|
||||
mergeAuxEventsWithDeletionBackfill,
|
||||
} from "./auxBackfill.ts";
|
||||
|
||||
@@ -126,3 +127,54 @@ test("merges deletion markers that target cached or fetched auxiliary event ids"
|
||||
[fetchedReactionId, cachedReactionDeletionId, fetchedReactionDeletionId],
|
||||
);
|
||||
});
|
||||
|
||||
test("fetchStructuralAuxForMessages returns edits plus their deletion closure", async () => {
|
||||
const replyId = hex("1");
|
||||
const editId = hex("2");
|
||||
const editDeletionId = hex("3");
|
||||
const edit = event(editId, 40003, {
|
||||
content: "edited text",
|
||||
tags: [
|
||||
["h", CHANNEL_ID],
|
||||
["e", replyId],
|
||||
],
|
||||
});
|
||||
const editDeletion = event(editDeletionId, 5, {
|
||||
tags: [
|
||||
["h", CHANNEL_ID],
|
||||
["e", editId],
|
||||
],
|
||||
});
|
||||
const auxCalls = [];
|
||||
const deletionCalls = [];
|
||||
|
||||
const auxEvents = await fetchStructuralAuxForMessages(CHANNEL_ID, [replyId], {
|
||||
fetchAuxEventsForMessages: async (channelId, ids) => {
|
||||
auxCalls.push({ channelId, ids });
|
||||
return [edit];
|
||||
},
|
||||
fetchAuxDeletionEventsForAuxEvents: async (channelId, ids) => {
|
||||
deletionCalls.push({ channelId, ids });
|
||||
return [editDeletion];
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(auxCalls, [{ channelId: CHANNEL_ID, ids: [replyId] }]);
|
||||
assert.deepEqual(deletionCalls, [{ channelId: CHANNEL_ID, ids: [editId] }]);
|
||||
assert.deepEqual(
|
||||
auxEvents.map((auxEvent) => auxEvent.id),
|
||||
[editId, editDeletionId],
|
||||
);
|
||||
});
|
||||
|
||||
test("fetchStructuralAuxForMessages skips all fetches for no message ids", async () => {
|
||||
const auxEvents = await fetchStructuralAuxForMessages(CHANNEL_ID, [], {
|
||||
fetchAuxEventsForMessages: async () => {
|
||||
throw new Error("must not fetch aux for an empty id set");
|
||||
},
|
||||
fetchAuxDeletionEventsForAuxEvents: async () => {
|
||||
throw new Error("must not fetch deletions for an empty id set");
|
||||
},
|
||||
});
|
||||
assert.deepEqual(auxEvents, []);
|
||||
});
|
||||
|
||||
@@ -65,6 +65,54 @@ export async function mergeAuxEventsWithDeletionBackfill(input: {
|
||||
return [...input.fetchedAuxEvents, ...auxDeletionEvents];
|
||||
}
|
||||
|
||||
/**
|
||||
* Structural aux closure (edits/deletions + deletions of those aux events)
|
||||
* for an explicit set of message ids, returned to the caller instead of being
|
||||
* merged into the channel cache. The thread-replies fetch uses this: the
|
||||
* server thread-subtree query resolves deletions itself but returns content
|
||||
* kinds only, so a reply's kind:40003 edit never rides along — without this
|
||||
* backfill a refetch (thread reopen, channel switch) renders the original,
|
||||
* un-edited text.
|
||||
*/
|
||||
export type StructuralAuxFetchDeps = {
|
||||
fetchAuxEventsForMessages: (
|
||||
channelId: string,
|
||||
messageIds: string[],
|
||||
) => Promise<RelayEvent[]>;
|
||||
fetchAuxDeletionEventsForAuxEvents: (
|
||||
channelId: string,
|
||||
auxEventIds: string[],
|
||||
) => Promise<RelayEvent[]>;
|
||||
};
|
||||
|
||||
const defaultStructuralAuxDeps: StructuralAuxFetchDeps = {
|
||||
fetchAuxEventsForMessages: (channelId, messageIds) =>
|
||||
relayClient.fetchAuxEventsByReference(
|
||||
channelId,
|
||||
messageIds,
|
||||
buildChannelStructuralAuxFilter,
|
||||
),
|
||||
fetchAuxDeletionEventsForAuxEvents: (channelId, auxEventIds) =>
|
||||
relayClient.fetchAuxDeletionEventsForAuxEvents(channelId, auxEventIds),
|
||||
};
|
||||
|
||||
export async function fetchStructuralAuxForMessages(
|
||||
channelId: string,
|
||||
messageIds: string[],
|
||||
deps: StructuralAuxFetchDeps = defaultStructuralAuxDeps,
|
||||
): Promise<RelayEvent[]> {
|
||||
if (messageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const auxEvents = await deps.fetchAuxEventsForMessages(channelId, messageIds);
|
||||
return mergeAuxEventsWithDeletionBackfill({
|
||||
channelId,
|
||||
cachedEvents: [],
|
||||
fetchedAuxEvents: auxEvents,
|
||||
fetchAuxEventsForMessages: deps.fetchAuxDeletionEventsForAuxEvents,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After a content-kinds-only history fetch, pull structural auxiliary events
|
||||
* (edits/deletions) that reference the loaded messages — keyed by `#e` over
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
collectMessageIdsForAuxBackfill,
|
||||
fetchStructuralAuxForMessages,
|
||||
} from "@/features/messages/lib/auxBackfill";
|
||||
import { threadRepliesKey } from "@/features/messages/lib/messageQueryKeys";
|
||||
import { getThreadReplies } from "@/shared/api/tauri";
|
||||
import type { Channel, RelayEvent, ThreadCursor } from "@/shared/api/types";
|
||||
@@ -7,6 +11,33 @@ import type { Channel, RelayEvent, ThreadCursor } from "@/shared/api/types";
|
||||
const THREAD_PAGE_LIMIT = 200;
|
||||
const MAX_THREAD_PAGES = 500;
|
||||
|
||||
/**
|
||||
* Append the structural aux closure (edits/deletions) for the fetched replies.
|
||||
* The server thread-subtree query resolves deletions itself but omits
|
||||
* kind:40003 edits, so a bare refetch would render every edited reply with its
|
||||
* original text. Best-effort: an aux failure logs and returns the replies
|
||||
* unadorned rather than failing the whole thread load.
|
||||
*/
|
||||
async function withStructuralAux(
|
||||
channelId: string,
|
||||
replies: RelayEvent[],
|
||||
): Promise<RelayEvent[]> {
|
||||
try {
|
||||
const auxEvents = await fetchStructuralAuxForMessages(
|
||||
channelId,
|
||||
collectMessageIdsForAuxBackfill(replies),
|
||||
);
|
||||
return auxEvents.length > 0 ? [...replies, ...auxEvents] : replies;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to backfill thread reply edits for channel",
|
||||
channelId,
|
||||
error,
|
||||
);
|
||||
return replies;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch a thread subtree into a cache independent from channel window pages. */
|
||||
export function useThreadReplies(
|
||||
activeChannel: Channel | null,
|
||||
@@ -31,7 +62,8 @@ export function useThreadReplies(
|
||||
{ limit: THREAD_PAGE_LIMIT, cursor },
|
||||
);
|
||||
replies.push(...response.events);
|
||||
if (!response.nextCursor) return replies;
|
||||
if (!response.nextCursor)
|
||||
return withStructuralAux(activeChannel.id, replies);
|
||||
cursor = response.nextCursor;
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user