From f76c44a7e6830f4fc6e431927ee65533c84bb2a9 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 12 Dec 2025 18:26:17 +0000 Subject: [PATCH] feat: add scroll state persistence to RantFeed with per-page state keys Implement saveScrollState, restoreScrollState, and clearScrollState methods in RantFeed component, storing anchor rant ID and scroll offset per page. Wire up initFeed calls in CollabsPage, HomePage, StoriesPage, WeeklyPage, and SearchPage to set unique state keys (feed_state_collabs, feed_state_home, etc.) enabling seamless scroll restoration on navigation back. --- js/components/rant-feed.js | 131 ++++++++++++++++++++++++++++++++++++- js/pages/collabs-page.js | 8 +++ js/pages/home-page.js | 8 +++ js/pages/search-page.js | 10 +++ js/pages/stories-page.js | 8 +++ js/pages/weekly-page.js | 8 +++ 6 files changed, 171 insertions(+), 2 deletions(-) diff --git a/js/components/rant-feed.js b/js/components/rant-feed.js index 906c4ec..0054f75 100644 --- a/js/components/rant-feed.js +++ b/js/components/rant-feed.js @@ -21,11 +21,130 @@ class RantFeed extends BaseComponent { this.sort = this.getAttr('sort') || 'recent'; this.feedType = this.getAttr('feed-type') || 'rants'; this.blockChangeHandler = () => this.render(); + this.stateKey = null; + this.pendingRestore = null; this.render(); this.bindEvents(); } + setStateKey(key) { + this.stateKey = key; + } + + getTopVisibleRant() { + const cards = this.$$('rant-card'); + if (!cards || cards.length === 0) return null; + + for (const card of cards) { + const rect = card.getBoundingClientRect(); + if (rect.top >= -rect.height && rect.top < window.innerHeight) { + const rantId = card.getRantId(); + const offsetFromTop = rect.top; + return { rantId, offsetFromTop }; + } + } + return null; + } + + saveScrollState() { + if (!this.stateKey) return; + + const topRant = this.getTopVisibleRant(); + if (!topRant) return; + + const state = { + anchorRantId: topRant.rantId, + offsetFromTop: topRant.offsetFromTop, + skip: this.skip, + sort: this.sort + }; + + this.getStorage()?.set(this.stateKey, state); + } + + clearScrollState() { + if (!this.stateKey) return; + this.getStorage()?.remove(this.stateKey); + } + + async restoreScrollState() { + if (!this.stateKey) return false; + + const state = this.getStorage()?.get(this.stateKey); + if (!state || state.sort !== this.sort) { + this.clearScrollState(); + return false; + } + + this.pendingRestore = state; + await this.loadWithLimit(state.skip || this.limit); + return true; + } + + scrollToAnchor() { + if (!this.pendingRestore) return; + + const { anchorRantId, offsetFromTop } = this.pendingRestore; + this.pendingRestore = null; + + requestAnimationFrame(() => { + const card = this.$(`rant-card[rant-id="${anchorRantId}"]`); + if (card) { + const rect = card.getBoundingClientRect(); + const scrollY = window.scrollY + rect.top - offsetFromTop; + window.scrollTo(0, Math.max(0, scrollY)); + } + this.clearScrollState(); + }); + } + + async loadWithLimit(limit) { + if (this.isLoading) return; + + this.rants = []; + this.skip = 0; + this.hasMore = true; + this.isLoading = true; + this.updateLoadingState(); + + try { + let result; + const api = this.getApi(); + + switch (this.feedType) { + case 'weekly': + result = await api?.getWeeklyRants(this.sort, limit, 0); + break; + case 'collabs': + result = await api?.getCollabs(this.sort, limit, 0); + break; + case 'stories': + result = await api?.getStories(this.sort, limit, 0); + break; + case 'search': + break; + default: + result = await api?.getRants(this.sort, limit, 0); + } + + if (result?.success) { + const newRants = result.rants || []; + this.rants = newRants; + this.skip = newRants.length; + this.hasMore = newRants.length >= limit; + } else { + this.hasMore = false; + } + } catch (error) { + this.hasMore = false; + } finally { + this.isLoading = false; + this.render(); + this.scrollToAnchor(); + } + } + getBlock() { return window.app?.block; } @@ -207,11 +326,15 @@ class RantFeed extends BaseComponent { this.intersectionObserver = observer; } - onConnected() { - this.load(true); + async onConnected() { + const restored = await this.restoreScrollState(); + if (!restored) { + this.load(true); + } } onDisconnected() { + this.saveScrollState(); if (this.intersectionObserver) { this.intersectionObserver.disconnect(); } @@ -223,10 +346,12 @@ class RantFeed extends BaseComponent { onAttributeChanged(name, oldValue, newValue) { if (name === 'sort' && oldValue !== newValue) { this.sort = newValue; + this.clearScrollState(); this.load(true); } if (name === 'feed-type' && oldValue !== newValue) { this.feedType = newValue; + this.clearScrollState(); this.load(true); } } @@ -234,12 +359,14 @@ class RantFeed extends BaseComponent { setSort(sort) { this.sort = sort; this.setAttr('sort', sort); + this.clearScrollState(); this.load(true); } setFeedType(type) { this.feedType = type; this.setAttr('feed-type', type); + this.clearScrollState(); this.load(true); } diff --git a/js/pages/collabs-page.js b/js/pages/collabs-page.js index 03e883a..2f8d3f0 100644 --- a/js/pages/collabs-page.js +++ b/js/pages/collabs-page.js @@ -10,6 +10,7 @@ import { BaseComponent } from '../components/base-component.js'; class CollabsPage extends BaseComponent { init() { this.render(); + this.initFeed(); } render() { @@ -23,6 +24,13 @@ class CollabsPage extends BaseComponent { `); } + initFeed() { + const feed = this.$('rant-feed'); + if (feed) { + feed.setStateKey('feed_state_collabs'); + } + } + refresh() { const feed = this.$('rant-feed'); if (feed) { diff --git a/js/pages/home-page.js b/js/pages/home-page.js index be22f10..3879b18 100644 --- a/js/pages/home-page.js +++ b/js/pages/home-page.js @@ -14,6 +14,7 @@ class HomePage extends BaseComponent { init() { this.render(); + this.initFeed(); } render() { @@ -26,6 +27,13 @@ class HomePage extends BaseComponent { `); } + initFeed() { + const feed = this.$('rant-feed'); + if (feed) { + feed.setStateKey('feed_state_home'); + } + } + onAttributeChanged(name, oldValue, newValue) { if (name === 'sort') { const feed = this.$('rant-feed'); diff --git a/js/pages/search-page.js b/js/pages/search-page.js index 355661c..b0d6ddd 100644 --- a/js/pages/search-page.js +++ b/js/pages/search-page.js @@ -13,6 +13,7 @@ class SearchPage extends BaseComponent { } init() { + this.currentQuery = null; this.render(); this.bindEvents(); } @@ -50,10 +51,19 @@ class SearchPage extends BaseComponent { `); if (query) { + this.currentQuery = query; + this.initFeed(); this.search(query); } } + initFeed() { + const feed = this.$('rant-feed'); + if (feed && this.currentQuery) { + feed.setStateKey('feed_state_search_' + this.currentQuery); + } + } + bindEvents() { this.on(this, 'submit', this.handleSubmit); } diff --git a/js/pages/stories-page.js b/js/pages/stories-page.js index d97ecc7..021216a 100644 --- a/js/pages/stories-page.js +++ b/js/pages/stories-page.js @@ -10,6 +10,7 @@ import { BaseComponent } from '../components/base-component.js'; class StoriesPage extends BaseComponent { init() { this.render(); + this.initFeed(); } render() { @@ -23,6 +24,13 @@ class StoriesPage extends BaseComponent { `); } + initFeed() { + const feed = this.$('rant-feed'); + if (feed) { + feed.setStateKey('feed_state_stories'); + } + } + refresh() { const feed = this.$('rant-feed'); if (feed) { diff --git a/js/pages/weekly-page.js b/js/pages/weekly-page.js index 8326009..17e1d82 100644 --- a/js/pages/weekly-page.js +++ b/js/pages/weekly-page.js @@ -10,6 +10,7 @@ import { BaseComponent } from '../components/base-component.js'; class WeeklyPage extends BaseComponent { init() { this.render(); + this.initFeed(); } render() { @@ -23,6 +24,13 @@ class WeeklyPage extends BaseComponent { `); } + initFeed() { + const feed = this.$('rant-feed'); + if (feed) { + feed.setStateKey('feed_state_weekly'); + } + } + refresh() { const feed = this.$('rant-feed'); if (feed) {