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.
This commit is contained in:
retoor 2025-12-12 18:26:17 +00:00
parent fefd47f703
commit f76c44a7e6
6 changed files with 171 additions and 2 deletions

View File

@ -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);
}

View File

@ -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) {

View File

@ -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');

View File

@ -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);
}

View File

@ -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) {

View File

@ -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) {