fix(desktop): preserve timeline scroll when opening threads (#1278)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-25 08:35:35 -07:00
committed by GitHub
co-authored by Pinky
parent 73cc31cc52
commit d05f122d8a
3 changed files with 11 additions and 1 deletions
@@ -669,6 +669,9 @@ export const ChannelPane = React.memo(function ChannelPane({
hasComposerOverlay={hasMainComposerOverlay}
hasOlderMessages={hasOlderMessages}
isFetchingOlder={isFetchingOlder}
layoutShiftKey={
useSplitAuxiliaryPane ? (openThreadHeadId ?? "closed") : "overlay"
}
isFollowingThreadById={isFollowingThreadById}
isMessageUnreadById={isMessageUnreadById}
personaLookup={personaLookup}
@@ -50,6 +50,7 @@ type MessageTimelineProps = {
/** True when the timeline has the composer overlay below it. */
hasComposerOverlay?: boolean;
isFetchingOlder?: boolean;
layoutShiftKey?: string | number | null;
messageFooters?: Record<string, React.ReactNode>;
/** Map from lowercase pubkey → persona display name for bot members. */
personaLookup?: Map<string, string>;
@@ -148,6 +149,7 @@ const MessageTimelineBase = React.forwardRef<
hasComposerOverlay = true,
hasOlderMessages = true,
isFetchingOlder = false,
layoutShiftKey = null,
followThreadById,
isFollowingThreadById,
isMessageUnreadById,
@@ -242,6 +244,7 @@ const MessageTimelineBase = React.forwardRef<
hasOlderMessages,
isFetchingOlder,
isLoading: showTimelineSkeleton,
layoutShiftKey,
messages: deferredMessages,
onTargetReached,
scrollContainerRef,
@@ -38,6 +38,8 @@ type UseAnchoredScrollOptions = {
* restoration re-run trigger so the anchor reasserts itself around the
* prepend on the fetch-state toggle, not only on the `messages` change. */
isFetchingOlder?: boolean;
/** Re-runs restoration when surrounding chrome changes size without changing messages. */
layoutShiftKey?: string | number | null;
/** When set, scroll to and highlight this message on mount and on change. */
targetMessageId?: string | null;
onTargetReached?: (messageId: string) => void;
@@ -212,6 +214,7 @@ export function useAnchoredScroll({
fetchOlder,
hasOlderMessages = false,
isFetchingOlder = false,
layoutShiftKey = null,
targetMessageId = null,
onTargetReached,
}: UseAnchoredScrollOptions): UseAnchoredScrollResult {
@@ -363,7 +366,7 @@ export function useAnchoredScroll({
// before the render. This is the single mechanism for keeping scroll
// stable across prepends, appends, image loads, embed expansions, etc.
// ---------------------------------------------------------------------------
// biome-ignore lint/correctness/useExhaustiveDependencies: `isFetchingOlder` is an intentional re-run trigger, not a read. It re-runs restoration on fetch-state toggles so the anchor reasserts itself around the prepend; the correction is a no-op when nothing above the anchor moved.
// biome-ignore lint/correctness/useExhaustiveDependencies: `isFetchingOlder` and `layoutShiftKey` are intentional re-run triggers, not reads. They re-run restoration when fetch state or surrounding layout changes so the anchor reasserts itself; the correction is a no-op when nothing above the anchor moved.
React.useLayoutEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
@@ -456,6 +459,7 @@ export function useAnchoredScroll({
}, [
isFetchingOlder,
isLoading,
layoutShiftKey,
messages,
onTargetReached,
scrollContainerRef,