diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 965f1e3b3a..c41ada17d1 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -262,7 +262,7 @@ export const ChannelPane = React.memo(function ChannelPane({ useComposerHeightPadding( timelineScrollRef, composerWrapperRef, - `${isSinglePanelView}:${hasMainComposerOverlay}`, + `${activeChannelId}:${isSinglePanelView}:${hasMainComposerOverlay}`, ); const clearWelcomeComposerDismissTimer = React.useCallback(() => { diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index e53fbdcd50..19a37ddf80 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -205,6 +205,12 @@ const MessageTimelineBase = React.forwardRef< const scrollRestorationId = targetMessageId ? `message-timeline:${channelId ?? "none"}:target:${targetMessageId}` : `message-timeline:${channelId ?? "none"}`; + // Keep the scroll node's DOM lifetime scoped to a channel. TanStack Router's + // scroll-restoration listener runs outside React and may write a saved + // scrollTop into the current scroll element during navigation; reusing the + // same node across channel routes can leave the newly-loaded message list + // painted at a stale offset until the user's next scroll event forces layout. + const scrollContainerDomKey = channelId ?? "none"; const timelineBodySurface = selectTimelineBodySurface({ deferredCount: deferredMessages.length, @@ -339,6 +345,7 @@ const MessageTimelineBase = React.forwardRef< )} data-scroll-restoration-id={scrollRestorationId} data-testid="message-timeline" + key={scrollContainerDomKey} onScroll={onScroll} ref={scrollContainerRef} >