mirror of
https://github.com/block/buzz.git
synced 2026-11-07 18:01:06 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
@@ -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<Set<string>>(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<string>();
|
||||
|
||||
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<RelayEvent[]>(
|
||||
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 ? (
|
||||
<EphemeralChannelBadge
|
||||
|
||||
@@ -42,7 +42,10 @@ function normalizeBranchReply(message: TimelineMessage): TimelineMessage {
|
||||
function buildSummaryParticipants(
|
||||
replies: TimelineMessage[],
|
||||
): TimelineThreadSummaryParticipant[] {
|
||||
const recentUniqueParticipants = new Map<string, TimelineThreadSummaryParticipant>();
|
||||
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,
|
||||
|
||||
@@ -74,7 +74,6 @@ export function MessageThreadPanel({
|
||||
threadHead,
|
||||
threadReplies,
|
||||
threadTypingPubkeys,
|
||||
totalReplyCount,
|
||||
}: MessageThreadPanelProps) {
|
||||
if (!threadHead) {
|
||||
return null;
|
||||
@@ -122,7 +121,10 @@ export function MessageThreadPanel({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto" data-testid="message-thread-body">
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
data-testid="message-thread-body"
|
||||
>
|
||||
<div className="px-3 py-3" data-testid="message-thread-head">
|
||||
<MessageRow
|
||||
activeReplyTargetId={replyTargetId}
|
||||
|
||||
@@ -13,10 +13,7 @@ function ParticipantAvatar({
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={index > 0 ? "-ml-2" : ""}
|
||||
style={{ zIndex: 10 - index }}
|
||||
>
|
||||
<div className={index > 0 ? "-ml-2" : ""} style={{ zIndex: 10 - index }}>
|
||||
<UserAvatar
|
||||
avatarUrl={participant.avatarUrl}
|
||||
className="rounded-full border-2 border-background"
|
||||
|
||||
106
desktop/src/features/messages/useLoadMissingAncestors.ts
Normal file
106
desktop/src/features/messages/useLoadMissingAncestors.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as React from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { channelMessagesKey } from "@/features/messages/lib/messageQueryKeys";
|
||||
import { mergeMessages } from "@/features/messages/hooks";
|
||||
import {
|
||||
getChannelIdFromTags,
|
||||
getThreadReference,
|
||||
} from "@/features/messages/lib/threading";
|
||||
import { getEventById } from "@/shared/api/tauri";
|
||||
import type { Channel, RelayEvent } from "@/shared/api/types";
|
||||
|
||||
export function useLoadMissingAncestors(
|
||||
activeChannel: Channel | null,
|
||||
resolvedMessages: RelayEvent[],
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const requestedAncestorIdsRef = React.useRef<Set<string>>(new Set());
|
||||
const previousChannelIdRef = React.useRef<string | null>(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<string>();
|
||||
|
||||
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<RelayEvent[]>(
|
||||
channelMessagesKey(activeChannel.id),
|
||||
(current = []) => mergeMessages(current, event),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load ancestor event", eventId, error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [activeChannel, queryClient, resolvedMessages]);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user