From b0e7163348260754940414359ed5d847766a8919 Mon Sep 17 00:00:00 2001 From: lens0021 Date: Sun, 21 Jun 2026 09:31:38 +0900 Subject: [PATCH] fix: retry divider centering until the feed actually renders on cold start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a notification cold start the boundary divider could fail to center, leaving the user stranded at the top with the "New posts above" seam off-screen below. Subscriptions load asynchronously (IndexedDB), so when boundaryNonce bumped the feed often wasn't rendered yet — the one-shot centering effect queried [data-divider], found nothing, and gave up. By the time subscriptions arrived and the feed (and divider) mounted, the nonce never changed again, so centering never retried. Arm a pending-center flag on the nonce bump and run it only once the [data-divider] element exists; the divider's own ref callback re-invokes it the moment it mounts, so a late-rendering feed still lands centered. The flag is consumed on success so it fires exactly once. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01VxfMbEXhsWRjjXEr5GMcF9 --- src/pages/SubscribedPage.tsx | 38 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/pages/SubscribedPage.tsx b/src/pages/SubscribedPage.tsx index 427ed47..1306201 100644 --- a/src/pages/SubscribedPage.tsx +++ b/src/pages/SubscribedPage.tsx @@ -1,7 +1,7 @@ import { faArrowsRotate } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import type { RefObject } from "react"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { AccountStatusGrid } from "../components/AccountStatusGrid"; import { FloatingRefreshButton } from "../components/FloatingRefreshButton"; import { PostList } from "../components/PostList"; @@ -57,19 +57,36 @@ export function SubscribedPage({ // seam, centered: the new/unseen posts above the fold, the already-seen ones // below. The cold-start case also overrides scroll-anchor restore for that // open (see useRestoreScrollAnchor's skip below) so the two don't fight. + // + // A bump arms a pending center rather than firing once-and-for-all: on a + // cold start the feed (and thus the divider) renders a beat after the nonce + // bumps, once the async-loaded subscriptions arrive. centerWhenReady centers + // only once the [data-divider] element actually exists, and the divider's own + // ref callback (onDividerRef below) re-invokes it the moment it mounts — so a + // late-rendering feed still lands centered instead of stranding the user at + // the top. The pending flag is consumed on success so it fires exactly once. + const pendingCenterRef = useRef(false); + const centerWhenReady = useCallback(() => { + if (!pendingCenterRef.current) return; + const el = scrollContainerRef.current; + if (!el || !el.querySelector("[data-divider]")) return; + pendingCenterRef.current = false; + centerScrollOnDivider(el); + }, [scrollContainerRef]); + const lastFlushNonce = useRef(flushNonce); const lastBoundaryNonce = useRef(boundaryNonce); useEffect(() => { if ( - flushNonce === lastFlushNonce.current && - boundaryNonce === lastBoundaryNonce.current - ) - return; - lastFlushNonce.current = flushNonce; - lastBoundaryNonce.current = boundaryNonce; - const el = scrollContainerRef.current; - if (el) requestAnimationFrame(() => centerScrollOnDivider(el)); - }, [flushNonce, boundaryNonce, scrollContainerRef]); + flushNonce !== lastFlushNonce.current || + boundaryNonce !== lastBoundaryNonce.current + ) { + lastFlushNonce.current = flushNonce; + lastBoundaryNonce.current = boundaryNonce; + pendingCenterRef.current = true; + } + centerWhenReady(); + }, [flushNonce, boundaryNonce, centerWhenReady]); // Track scroll position for the floating button and the status grid: // - the status grid and the top-of-feed signals show only at the very top @@ -177,6 +194,7 @@ export function SubscribedPage({ accessToken={accessToken} scrollContainerRef={scrollContainerRef} dividerPostId={dividerPostId} + onDividerRef={centerWhenReady} /> )}