diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 9eedcf252c..93deda4c80 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -33,7 +33,7 @@ const overrides = new Map([ ["src/app/AppShell.tsx", 750], ["src/features/channels/hooks.ts", 525], // canvas query + mutation hooks ["src/features/channels/ui/ChannelManagementSheet.tsx", 800], - ["src/features/messages/ui/MessageComposer.tsx", 650], // media upload handlers (paste, drop, dialog) + ["src/features/messages/ui/MessageComposer.tsx", 665], // media upload handlers (paste, drop, dialog) + channelId reset effect ["src/features/settings/ui/SettingsView.tsx", 600], ["src/features/sidebar/ui/AppSidebar.tsx", 850], // channels + forums creation forms ["src/features/tokens/ui/TokenSettingsCard.tsx", 800], diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 20d75f44ea..e080b41a30 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -217,14 +217,6 @@ export function AppShell() { .filter((value) => value && value.trim().length > 0) .join(" ") || "Channel details and activity." : "Connect to the relay to browse channels and read messages."; - const contentPaneKey = - selectedView === "home" - ? "home" - : selectedView === "agents" - ? "agents" - : selectedView === "settings" - ? "settings" - : `channel:${activeChannel?.id ?? "none"}`; const shouldLoadTimeline = activeChannel !== null && activeChannel.channelType !== "forum"; const isTimelineLoading = @@ -589,10 +581,7 @@ export function AppShell() { unreadChannelIds={unreadChannelIds} /> - + {selectedView === "home" ? ( + ({ status: "idle" }); const [pendingImeta, setPendingImeta] = React.useState([]); + // biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the sole trigger — reset all composer state on channel switch to prevent draft/upload/mention leaks + React.useEffect(() => { + setContent(""); + setPendingImeta([]); + setUploadState({ status: "idle" }); + setMentionQuery(null); + setMentionStartIndex(0); + setMentionSelectedIndex(0); + setIsEmojiPickerOpen(false); + mentionMapRef.current.clear(); + draftSelectionRef.current = { end: 0, start: 0 }; + pendingSelectionRef.current = null; + }, [channelId]); + const membersQuery = useChannelMembersQuery(channelId); const members = membersQuery.data ?? []; const managedAgentsQuery = useManagedAgentsQuery(); diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 1a991d43ec..17d6937c8f 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -11,6 +11,7 @@ import { TimelineSkeleton } from "./TimelineSkeleton"; import { useTimelineScrollManager } from "./useTimelineScrollManager"; type MessageTimelineProps = { + channelId?: string | null; messages: TimelineMessage[]; isLoading?: boolean; emptyTitle?: string; @@ -29,6 +30,7 @@ type MessageTimelineProps = { }; export function MessageTimeline({ + channelId, messages, isLoading = false, emptyTitle = "No messages yet", @@ -51,6 +53,7 @@ export function MessageTimeline({ syncScrollState, timelineRef, } = useTimelineScrollManager({ + channelId, isLoading, messages, onTargetReached, diff --git a/desktop/src/features/messages/ui/useTimelineScrollManager.ts b/desktop/src/features/messages/ui/useTimelineScrollManager.ts index b7b2b8de23..ff6ca211bf 100644 --- a/desktop/src/features/messages/ui/useTimelineScrollManager.ts +++ b/desktop/src/features/messages/ui/useTimelineScrollManager.ts @@ -4,11 +4,13 @@ import type { TimelineMessage } from "@/features/messages/types"; import { isNearBottom } from "./messageTimelineUtils"; export function useTimelineScrollManager({ + channelId, isLoading, messages, onTargetReached, targetMessageId, }: { + channelId?: string | null; isLoading: boolean; messages: TimelineMessage[]; onTargetReached?: (messageId: string) => void; @@ -32,6 +34,24 @@ export function useTimelineScrollManager({ string | null >(null); const [newMessageCount, setNewMessageCount] = React.useState(0); + + // biome-ignore lint/correctness/useExhaustiveDependencies: channelId is intentionally the sole trigger — we reset all scroll state when the channel changes + React.useLayoutEffect(() => { + hasInitializedRef.current = false; + shouldStickToBottomRef.current = true; + isAtBottomRef.current = true; + isProgrammaticBottomScrollRef.current = false; + previousTimelineHeightRef.current = null; + previousScrollTopRef.current = 0; + lockedScrollTopRef.current = null; + previousLastMessageIdRef.current = undefined; + previousMessageCountRef.current = 0; + handledTargetMessageIdRef.current = null; + setIsAtBottom(true); + setHighlightedMessageId(null); + setNewMessageCount(0); + }, [channelId]); + const latestMessage = messages.length > 0 ? messages[messages.length - 1] : undefined; diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx index 6831a4dfb0..579da46e67 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/main.tsx @@ -13,6 +13,10 @@ const queryClient = new QueryClient({ queries: { retry: 1, refetchOnWindowFocus: false, + networkMode: "always", + }, + mutations: { + networkMode: "always", }, }, });