atagen

malice

public
Star0
Watch0

Commit 11cc2db16d17

tarball

atagen <boss@atagen.co>2026-10-24 12:38 UTC

1 file changed+49-4
web: add navigation history and preserve pagination position
Modified file: web/src/App.svelte+49 additions−4 deletions
@@ -157,6 +157,7 @@
157157 let spaceChildrenLoading = false;
158158 let spaceChildrenError: string | null = null;
159159 let avatarByUser: Record<string, string | null> = {};
160+ let restoringRoute = false;
160161
161162 // account data is read from the local cache, which is empty on a fresh device until the first sync lands
162163 $: {
@@ -291,9 +292,47 @@
291292 return r.unread_messages + r.unread_mentions;
292293 }
293294
295+ function recordRoute(spaceId: string | null, roomId: string | null): void {
296+ if (restoringRoute) return;
297+ const url = new URL(window.location.href);
298+ url.searchParams.delete('space');
299+ url.searchParams.delete('room');
300+ if (spaceId) url.searchParams.set('space', spaceId);
301+ if (roomId) url.searchParams.set('room', roomId);
302+ const next = `${url.pathname}${url.search}${url.hash}`;
303+ const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
304+ if (next !== current) window.history.pushState({ spaceId, roomId }, '', next);
305+ }
306+
307+ async function restoreRoute(): Promise<void> {
308+ const url = new URL(window.location.href);
309+ const roomId = url.searchParams.get('room');
310+ const spaceId = url.searchParams.get('space');
311+ restoringRoute = true;
312+ try {
313+ if (roomId && $rooms.some((room) => room.room_id === roomId && !room.space)) {
314+ selectedSpaceId = spaceId && $rooms.some((room) => room.room_id === spaceId && room.space) ? spaceId : null;
315+ spaceChildrenError = null;
316+ spaceChildIds = selectedSpaceId ? (await getSpaceChildren(selectedSpaceId)).room_ids ?? [] : null;
317+ await selectRoom(roomId);
318+ } else if (spaceId && $rooms.some((room) => room.room_id === spaceId && room.space)) {
319+ await selectSpace(spaceId);
320+ } else {
321+ await selectHome();
322+ }
323+ } catch (e) {
324+ pushBanner(e instanceof Error ? e.message : 'Could not restore navigation');
325+ } finally {
326+ restoringRoute = false;
327+ }
328+ }
329+
294330 onMount(() => {
331+ const onPopState = () => void restoreRoute();
332+ window.addEventListener('popstate', onPopState);
295333 void boot();
296334 return () => {
335+ window.removeEventListener('popstate', onPopState);
297336 disconnectEvents();
298337 stopTyping();
299338 if (readTimer !== null) window.clearTimeout(readTimer);
@@ -308,6 +347,7 @@
308347 if (session.user_id) {
309348 softLogout.set(null);
310349 await refreshRooms();
350+ await restoreRoute();
311351 connectEvents();
312352 void loadProfile();
313353 void loadEncryption();
@@ -451,6 +491,7 @@
451491 spaceChildrenError = null;
452492 if (window.innerWidth > 640) navFolded.set(false);
453493 sidebarOpen = true;
494+ recordRoute(null, null);
454495 }
455496
456497 function toggleChannelsPane(): void {
@@ -504,6 +545,7 @@
504545 const spaceId = selectedSpaceId;
505546 if (spaceId) lastSpaceRooms.update((all) => ({ ...all, [spaceId]: roomId }));
506547 selectedRoomId.set(roomId);
548+ recordRoute(selectedSpaceId, roomId);
507549 composer = drafts[roomId] ?? '';
508550 replyingTo = null;
509551 editing = null;
@@ -563,6 +605,7 @@
563605
564606 let userScrollUntil = 0;
565607 let draggingScrollbar = false;
608+ let paginationAnchor: { roomId: string; top: number; height: number } | null = null;
566609
567610 function noteUserScroll(): void {
568611 userScrollUntil = performance.now() + 300;
@@ -581,6 +624,7 @@
581624 const gap = timelineEl.scrollHeight - timelineEl.scrollTop - timelineEl.clientHeight;
582625 if (gap < 80) stickToBottom = true;
583626 else if (draggingScrollbar || performance.now() < userScrollUntil) stickToBottom = false;
627+ if (timelineEl.scrollTop < 80 && !hasStart && !paginateBusy) void paginate();
584628 }
585629
586630 function pinTimeline(node: HTMLElement) {
@@ -597,7 +641,10 @@
597641 if (!roomId || list.length === 0) return;
598642 if (roomId !== $selectedRoomId) return;
599643 await tick();
600- if (stickToBottom) scrollToBottom();
644+ if (paginationAnchor?.roomId === roomId && timelineEl && !stickToBottom) {
645+ timelineEl.scrollTop = paginationAnchor.top + timelineEl.scrollHeight - paginationAnchor.height;
646+ paginationAnchor = null;
647+ } else if (stickToBottom) scrollToBottom();
601648 queueReadReceipt();
602649 }
603650
@@ -1227,6 +1274,7 @@
12271274 async function paginate(): Promise<void> {
12281275 const roomId = $selectedRoomId;
12291276 if (!roomId || paginateBusy) return;
1277+ if (timelineEl && !stickToBottom) paginationAnchor = { roomId, top: timelineEl.scrollTop, height: timelineEl.scrollHeight };
12301278 paginateBusy = true;
12311279 try {
12321280 await paginateRoom(roomId);
@@ -1870,9 +1918,6 @@
18701918 </button>
18711919 {#if roomMenuOpen}
18721920 <div class="room-menu" role="menu">
1873- <button type="button" role="menuitem" on:click={() => roomMenuAction(() => (dialog = 'security'))}>
1874- Security
1875- </button>
18761921 {#if selectedRoom.membership === 'Joined'}
18771922 <button
18781923 type="button"