From 67e39e46352ead21d79946b1030d9108d9331a8a Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Mon, 15 Jun 2026 23:02:09 -0700 Subject: [PATCH] fix(timeline): let channel intro header be earned, not asserted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 78f81e89 `topAlignIntro` flag force-painted the channel intro flush to the viewport top whenever the intro showed, dropping the bottom-pin (`min-h-full` + `mt-auto`). That's a different flavor of the step-5 lie: it asserts "this is the top" before the top is determined — during first-load the list is still streaming in from the bottom and where the top sits isn't knowable yet. The intro already lives in-flow ABOVE the virtualized list and `scrollMargin` already accounts for its height, so a bottom-anchored list naturally scrolls the intro off the top. The fix is purely to unwind the flush-top CSS: the intro shares the bottom-pin like the DM intro / generic empty, so the intro+list block fills the container and pins to the bottom, leaving the intro scrolled off-top until the user scrolls up to it or the top naturally loads in. The header is then EARNED by reaching the genuine top, not painted up front. Main timeline only; DM intro + generic empty untouched. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../features/messages/ui/MessageTimeline.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 935f907ef..a9b5ec367 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -331,13 +331,18 @@ export const MessageTimeline = React.memo(function MessageTimeline({ const showChannelIntro = !isLoading && channelIntro !== null && directMessageIntro === null; const showIntro = showDirectMessageIntro || showChannelIntro; - // The channel intro is an in-list HEADER: it must sit FLUSH at the top of the - // virtualized list (the `#` avatar / title / "beginning of…" / action cards), - // not float bottom-pinned in the viewport like the DM intro / empty state. So - // for the channel-intro case we drop the `min-h-full` fill + the intro's - // `mt-auto`, letting natural top-down flow place it at the top. Everything - // else (DM intro, generic empty) keeps the existing bottom-pin behavior. - const topAlignIntro = showChannelIntro; + // The channel intro is the TERMINAL header of a bottom-anchored list — the + // thing you reach only once you've genuinely arrived at the true top. It must + // NOT be force-painted flush at the viewport top during first-load, because + // at that point "where the top is" isn't even determined yet (the list is + // still streaming in from the bottom). So the intro lives in-flow ABOVE the + // virtualized list and shares the bottom-pin (`min-h-full` + `mt-auto`): the + // intro+list block fills the container and pins to the bottom, leaving the + // intro scrolled off the top until the user scrolls up to it or the top + // naturally loads into view. The header is then EARNED by reaching the top, + // not asserted up front. (The earlier `topAlignIntro` flush-top flag was a + // different flavor of the step-5 lie — asserting "this is the top" before the + // top was determined — and is gone.) const showGenericEmpty = !isLoading && deferredMessages.length === 0 && @@ -367,7 +372,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({ className={cn( "flex w-full flex-col gap-2", channelChrome.contentPadding, - (showIntro || showGenericEmpty) && !topAlignIntro && "min-h-full", + (showIntro || showGenericEmpty) && "min-h-full", )} >
@@ -381,16 +386,12 @@ export const MessageTimeline = React.memo(function MessageTimeline({ } @@ -418,12 +419,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({ {showChannelIntro ? (