fix(shell): scroll restore on Back + reset on new filter (bugs 1, 3)

The shell owns the scroller, so it owns the scroll policy. PageScroller
now takes a scrollKey identifying the logical view and keeps a per-view
offset in a module-level map, recorded live on scroll:
- same key returning (feed -> channel -> Back, filters unchanged) restores
  where you were (bug 1) -- the feed's scroller unmounts, but the saved
  offset survives and is re-applied, retried over a few frames so the
  virtualized list has time to reach its full height;
- a new key (feed filters changed) starts at the top of the fresh result
  set (bug 3). q is normalised out of the feed key so typing doesn't reset.

Recording on scroll rather than at unmount matters: by cleanup time the
scroller's scrollTop is already zeroed, so a cleanup-read saved 0.

Also fixes a Maximum-update-depth loop this introduced: the combined
element/fade ref was an inline callback, so React re-attached it every
commit and re-ran useScrollFade's state setter. Bound it with useCallback.

Adds E2E locks for bugs 1, 3 and 4 (restore on Back, reset on filter,
toolbar stays pinned). Full suite 14/14 green on :5173; tsc + knip clean.
This commit is contained in:
2026-07-18 03:51:40 +02:00
parent a6cbc45334
commit 10f4baab3b
5 changed files with 158 additions and 9 deletions
+13 -1
View File
@@ -632,6 +632,17 @@ export default function App() {
<div className="flex-1 grid place-items-center text-muted">{t("common.loading")}</div>
);
// Scroll restore/reset key for the shared normal-page scroller (see PageScroller): the same
// page/feed-filters returns you to where you were (bug 1); a changed feed filter is a new key, so
// the fresh result set starts at the top (bug 3). `q` is normalised out so typing in the search
// box doesn't reset the scroll on every keystroke; the live-YouTube sub-view gets its own key.
const normalScrollKey =
page !== "feed"
? `page:${page}`
: ytSearch != null
? `yt:${ytSearch}`
: `feed:${JSON.stringify({ ...filters, q: "" })}`;
return (
<div className="h-screen flex text-fg">
<NavSidebar
@@ -689,7 +700,7 @@ export default function App() {
)}
<div className="relative flex-1 min-w-0 flex flex-col">
{channelView ? (
<PageShell>
<PageShell scrollKey={`channel:${channelView.id}`}>
<Suspense fallback={pageFallback}>
<ChannelPage
key={channelView.id}
@@ -718,6 +729,7 @@ export default function App() {
</PageShell>
) : (
<PageShell
scrollKey={normalScrollKey}
header={
<Header
me={meQuery.data!}