From f04b7c807e5cf41919b47e3fb88cf0f552436076 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 13 Apr 2026 20:33:44 -0400 Subject: [PATCH] fix(desktop): unblock nested thread panel CI Clean up the nested thread panel follow-up changes so the desktop checks pass again by removing the unused thread prop, extracting ancestor loading out of ChannelScreen, and aligning the affected files with the current formatting and size guardrails. Made-with: Cursor --- desktop/scripts/check-file-sizes.mjs | 2 +- .../features/channels/ui/ChannelScreen.tsx | 112 ++---------------- .../src/features/messages/lib/threadPanel.ts | 18 ++- .../messages/ui/MessageThreadPanel.tsx | 6 +- .../messages/ui/MessageThreadSummaryRow.tsx | 5 +- .../messages/useLoadMissingAncestors.ts | 106 +++++++++++++++++ desktop/src/shared/api/relayClientSession.ts | 8 +- desktop/tests/e2e/messaging.spec.ts | 6 +- 8 files changed, 139 insertions(+), 124 deletions(-) create mode 100644 desktop/src/features/messages/useLoadMissingAncestors.ts diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index dec95f61cf..6b5dd5b4ea 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -40,7 +40,7 @@ const overrides = new Map([ ["src/features/settings/ui/SettingsView.tsx", 600], ["src/features/sidebar/ui/AppSidebar.tsx", 860], // channels + forums creation forms + Pulse nav ["src/features/tokens/ui/TokenSettingsCard.tsx", 800], - ["src/shared/api/relayClientSession.ts", 790], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator + fetchChannelHistoryBefore + ["src/shared/api/relayClientSession.ts", 792], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator + fetchChannelHistoryBefore; temporarily raised to cover subscription replay hardening on human-reply ["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions ["src-tauri/src/lib.rs", 570], // sprout-media:// proxy + Range headers + Sprout nest init (ensure_nest) in setup() + persona pack commands ["src-tauri/src/commands/media.rs", 720], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index fab898975b..52366919e7 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -1,6 +1,4 @@ import * as React from "react"; -import { useQueryClient } from "@tanstack/react-query"; - import { useAppShell } from "@/app/AppShellContext"; import { ChatHeader } from "@/features/chat/ui/ChatHeader"; import { useActiveChannelHeader } from "@/features/channels/useActiveChannelHeader"; @@ -22,22 +20,17 @@ import { useSendMessageMutation, useToggleReactionMutation, } from "@/features/messages/hooks"; -import { channelMessagesKey } from "@/features/messages/lib/messageQueryKeys"; import { collectMessageAuthorPubkeys, formatTimelineMessages, } from "@/features/messages/lib/formatTimelineMessages"; -import { - getChannelIdFromTags, - getThreadReference, -} from "@/features/messages/lib/threading"; import { buildThreadPanelData } from "@/features/messages/lib/threadPanel"; import { useFetchOlderMessages } from "@/features/messages/useFetchOlderMessages"; +import { useLoadMissingAncestors } from "@/features/messages/useLoadMissingAncestors"; import { useChannelTyping } from "@/features/messages/useChannelTyping"; import { PresenceBadge } from "@/features/presence/ui/PresenceBadge"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import { mergeCurrentProfileIntoLookup } from "@/features/profile/lib/identity"; -import { getEventById } from "@/shared/api/tauri"; import type { Channel, Identity, @@ -79,7 +72,6 @@ export function ChannelScreen({ targetMessageEvent, targetMessageId, }: ChannelScreenProps) { - const queryClient = useQueryClient(); const { markChannelRead, openChannelManagement } = useAppShell(); const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false); const [threadHeadPath, setThreadHeadPath] = React.useState([]); @@ -159,13 +151,12 @@ export function ChannelScreen({ [openThreadHeadId, typingEntries], ); const messageProfilePubkeys = React.useMemo( - () => - [ - ...new Set([ - ...messageAuthorPubkeys, - ...typingEntries.map((entry) => entry.pubkey), - ]), - ], + () => [ + ...new Set([ + ...messageAuthorPubkeys, + ...typingEntries.map((entry) => entry.pubkey), + ]), + ], [messageAuthorPubkeys, typingEntries], ); const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, { @@ -309,7 +300,6 @@ export function ChannelScreen({ shouldLoadTimeline && (messagesQuery.isPending || (messagesQuery.isFetching && resolvedMessages.length === 0)); - const requestedAncestorIdsRef = React.useRef>(new Set()); const resetComposerTargets = React.useCallback( (_channelId: string | null) => { setThreadHeadPath([]); @@ -318,12 +308,6 @@ export function ChannelScreen({ }, [], ); - const resetRequestedAncestors = React.useCallback( - (_channelId: string | null) => { - requestedAncestorIdsRef.current.clear(); - }, - [], - ); React.useEffect(() => { resetComposerTargets(activeChannelId); @@ -351,91 +335,11 @@ export function ChannelScreen({ editTargetMessage, openThreadHeadId, openThreadHeadMessage, - threadHeadPath, threadReplyTargetId, threadReplyTargetMessage, ]); - React.useEffect(() => { - resetRequestedAncestors(activeChannelId); - }, [activeChannelId, resetRequestedAncestors]); - - React.useEffect(() => { - if (!activeChannel || activeChannel.channelType === "forum") { - return; - } - - const knownEvents = new Map( - resolvedMessages.map((message) => [message.id, message]), - ); - const missingAncestorIds = new Set(); - - for (const message of resolvedMessages) { - const thread = getThreadReference(message.tags); - - for (const eventId of [thread.parentId, thread.rootId]) { - if ( - !eventId || - knownEvents.has(eventId) || - requestedAncestorIdsRef.current.has(eventId) - ) { - continue; - } - - missingAncestorIds.add(eventId); - } - } - - if (missingAncestorIds.size === 0) { - return; - } - - for (const eventId of missingAncestorIds) { - requestedAncestorIdsRef.current.add(eventId); - } - - const maxRequestedAncestors = 500; - if (requestedAncestorIdsRef.current.size > maxRequestedAncestors) { - const excess = - requestedAncestorIdsRef.current.size - maxRequestedAncestors; - let removed = 0; - for (const id of requestedAncestorIdsRef.current) { - if (removed >= excess) { - break; - } - requestedAncestorIdsRef.current.delete(id); - removed++; - } - } - - let isCancelled = false; - - void Promise.all( - [...missingAncestorIds].map(async (eventId) => { - try { - const event = await getEventById(eventId); - - if ( - isCancelled || - getChannelIdFromTags(event.tags) !== activeChannel.id - ) { - return; - } - - queryClient.setQueryData( - channelMessagesKey(activeChannel.id), - (current = []) => mergeMessages(current, event), - ); - } catch (error) { - console.error("Failed to load ancestor event", eventId, error); - } - }), - ); - - return () => { - isCancelled = true; - }; - }, [activeChannel, queryClient, resolvedMessages]); + useLoadMissingAncestors(activeChannel, resolvedMessages); const activeChannelEphemeralBadge = activeChannelEphemeralDisplay ? ( (); + const recentUniqueParticipants = new Map< + string, + TimelineThreadSummaryParticipant + >(); for (let index = replies.length - 1; index >= 0; index -= 1) { const reply = replies[index]; @@ -107,7 +110,10 @@ export function buildMainTimelineEntries( .map((message) => { return { message, - summary: buildSummaryForDirectReplies(message.id, directChildrenByParentId), + summary: buildSummaryForDirectReplies( + message.id, + directChildrenByParentId, + ), }; }); } @@ -140,9 +146,9 @@ export function buildThreadPanelData( const directChildrenByParentId = buildDirectChildrenByParentId(messages); const normalizedThreadHead = normalizeHeadMessage(threadHead); - const directReplies = (directChildrenByParentId.get(openThreadHeadId) ?? []).map( - (message) => normalizeBranchReply(message), - ); + const directReplies = ( + directChildrenByParentId.get(openThreadHeadId) ?? [] + ).map((message) => normalizeBranchReply(message)); const visibleReplies = directReplies.map((message) => ({ message, summary: buildSummaryForDirectReplies(message.id, directChildrenByParentId), @@ -151,7 +157,7 @@ export function buildThreadPanelData( const replyTargetInBranch = threadReplyTargetId === threadHead.id ? normalizedThreadHead - : messageById.get(threadReplyTargetId ?? "") ?? null; + : (messageById.get(threadReplyTargetId ?? "") ?? null); return { threadHead: normalizedThreadHead, diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index 6e46ad5609..78badbe476 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -74,7 +74,6 @@ export function MessageThreadPanel({ threadHead, threadReplies, threadTypingPubkeys, - totalReplyCount, }: MessageThreadPanelProps) { if (!threadHead) { return null; @@ -122,7 +121,10 @@ export function MessageThreadPanel({ -
+
0 ? "-ml-2" : ""} - style={{ zIndex: 10 - index }} - > +
0 ? "-ml-2" : ""} style={{ zIndex: 10 - index }}> >(new Set()); + const previousChannelIdRef = React.useRef(null); + + React.useEffect(() => { + const activeChannelId = activeChannel?.id ?? null; + if (previousChannelIdRef.current === activeChannelId) { + return; + } + previousChannelIdRef.current = activeChannelId; + requestedAncestorIdsRef.current.clear(); + }, [activeChannel?.id]); + + React.useEffect(() => { + if (!activeChannel || activeChannel.channelType === "forum") { + return; + } + + const knownEvents = new Map( + resolvedMessages.map((message) => [message.id, message]), + ); + const missingAncestorIds = new Set(); + + for (const message of resolvedMessages) { + const thread = getThreadReference(message.tags); + + for (const eventId of [thread.parentId, thread.rootId]) { + if ( + !eventId || + knownEvents.has(eventId) || + requestedAncestorIdsRef.current.has(eventId) + ) { + continue; + } + + missingAncestorIds.add(eventId); + } + } + + if (missingAncestorIds.size === 0) { + return; + } + + for (const eventId of missingAncestorIds) { + requestedAncestorIdsRef.current.add(eventId); + } + + const maxRequestedAncestors = 500; + if (requestedAncestorIdsRef.current.size > maxRequestedAncestors) { + const excess = + requestedAncestorIdsRef.current.size - maxRequestedAncestors; + let removed = 0; + for (const id of requestedAncestorIdsRef.current) { + if (removed >= excess) { + break; + } + requestedAncestorIdsRef.current.delete(id); + removed++; + } + } + + let isCancelled = false; + + void Promise.all( + [...missingAncestorIds].map(async (eventId) => { + try { + const event = await getEventById(eventId); + + if ( + isCancelled || + getChannelIdFromTags(event.tags) !== activeChannel.id + ) { + return; + } + + queryClient.setQueryData( + channelMessagesKey(activeChannel.id), + (current = []) => mergeMessages(current, event), + ); + } catch (error) { + console.error("Failed to load ancestor event", eventId, error); + } + }), + ); + + return () => { + isCancelled = true; + }; + }, [activeChannel, queryClient, resolvedMessages]); +} diff --git a/desktop/src/shared/api/relayClientSession.ts b/desktop/src/shared/api/relayClientSession.ts index 1a4ef9fe93..6069ceea13 100644 --- a/desktop/src/shared/api/relayClientSession.ts +++ b/desktop/src/shared/api/relayClientSession.ts @@ -21,10 +21,10 @@ import { } from "@/shared/api/relayClientShared"; import { buildThreadReferenceTags } from "@/features/messages/lib/threading"; -const RECONNECT_BASE_DELAY_MS = 1_000; -const RECONNECT_MAX_DELAY_MS = 30_000; -const RECONNECT_REPLAY_SKEW_SECS = 5; -const EVENT_BATCH_MS = 16; +const RECONNECT_BASE_DELAY_MS = 1_000, + RECONNECT_MAX_DELAY_MS = 30_000; +const RECONNECT_REPLAY_SKEW_SECS = 5, + EVENT_BATCH_MS = 16; export class RelayClient { private wsId: number | null = null; diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index 87af439819..5262893213 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -331,9 +331,9 @@ test("opens a branch-only thread panel from the reply action", async ({ await expect(threadPanel.getByTestId("message-thread-head")).toContainText( firstReply, ); - await expect(threadPanel.getByTestId("message-thread-head")).not.toContainText( - "Welcome to #general", - ); + await expect( + threadPanel.getByTestId("message-thread-head"), + ).not.toContainText("Welcome to #general"); await expect(threadReplies).not.toContainText(siblingReply); await threadComposer.fill(nestedReply);