Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): persist scroll position on navigation back to album #11388

Merged
merged 10 commits into from
Nov 25, 2024
7 changes: 6 additions & 1 deletion web/src/lib/components/layouts/user-page-layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
export let scrollbar = true;
export let admin = false;

export let scrollSlot: HTMLDivElement | undefined = undefined;

$: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden';
$: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
</script>
Expand Down Expand Up @@ -49,7 +51,10 @@
</div>
{/if}

<div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto">
<div
class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto"
bind:this={scrollSlot}
>
<slot />
</div>
</section>
Expand Down
5 changes: 5 additions & 0 deletions web/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export enum QueryParameter {
PAGE = 'page',
}

export enum SessionStorageKey {
INFINITE_SCROLL_PAGE = 'infiniteScrollPage',
SCROLL_POSITION = 'scrollPosition',
}

export enum OpenSettingQueryParameterValue {
OAUTH = 'oauth',
JOB = 'job',
Expand Down
25 changes: 24 additions & 1 deletion web/src/routes/(user)/albums/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import { beforeNavigate } from '$app/navigation';
import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store';
import { createAlbumAndRedirect } from '$lib/utils/album-utils';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
Expand All @@ -8,15 +9,37 @@
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import GroupTab from '$lib/components/elements/group-tab.svelte';
import SearchBar from '$lib/components/elements/search-bar.svelte';
import { AppRoute, SessionStorageKey } from '$lib/constants';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';

export let data: PageData;

let scrollSlot: HTMLDivElement;
beforeNavigate(({ to }) => {
// Save current scroll information when going into a person page.
if (to && to.url.pathname.startsWith(AppRoute.ALBUMS)) {
sessionStorage.setItem(SessionStorageKey.SCROLL_POSITION, scrollSlot.scrollTop.toString());
} else {
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
}
});
onMount(() => {
let newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
if (newScroll) {
scrollSlot.scroll({
top: Number.parseFloat(newScroll),
behavior: 'instant',
});
}
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
});

let searchQuery = '';
let albumGroups: string[] = [];
</script>

<UserPageLayout title={data.meta.title}>
<UserPageLayout title={data.meta.title} bind:scrollSlot>
<div class="flex place-items-center gap-2" slot="buttons">
<AlbumsControls {albumGroups} bind:searchQuery />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { afterNavigate, goto, onNavigate } from '$app/navigation';
import { afterNavigate, beforeNavigate, goto, onNavigate } from '$app/navigation';
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
Expand Down Expand Up @@ -32,7 +32,7 @@
notificationController,
} from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute } from '$lib/constants';
import { AppRoute, SessionStorageKey } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
Expand Down Expand Up @@ -138,6 +138,13 @@

$: albumHasViewers = album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer);

beforeNavigate(({ to }) => {
// Forget scroll position from albums page if going somewhere else.
if (to && !to.url.pathname.startsWith(AppRoute.ALBUMS)) {
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
}
});

afterNavigate(({ from }) => {
let url: string | undefined = from?.url?.pathname;

Expand Down
57 changes: 55 additions & 2 deletions web/src/routes/(user)/people/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { beforeNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import { focusTrap } from '$lib/actions/focus-trap';
import Button from '$lib/components/elements/buttons/button.svelte';
Expand All @@ -17,7 +17,7 @@
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
import { locale } from '$lib/stores/preferences.store';
import { websocketEvents } from '$lib/stores/websocket';
import { handlePromiseError } from '$lib/utils';
Expand Down Expand Up @@ -61,12 +61,64 @@
let changeNameInputEl: HTMLInputElement | null;
let innerHeight: number;

let scrollSlot: HTMLDivElement;
beforeNavigate(({ to }) => {
// Save current scroll information when going into a person page.
if (to && to.url.pathname.startsWith(AppRoute.PEOPLE)) {
if (nextPage) {
sessionStorage.setItem(SessionStorageKey.INFINITE_SCROLL_PAGE, nextPage.toString());
}
sessionStorage.setItem(SessionStorageKey.SCROLL_POSITION, scrollSlot.scrollTop.toString());
} else {
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
}
});

const restoreScrollPosition = () => {
let newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
if (newScroll) {
scrollSlot.scroll({
top: Number.parseFloat(newScroll),
behavior: 'instant',
});
}
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
};

onMount(() => {
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
if (getSearchedPeople) {
searchName = getSearchedPeople;
handlePromiseError(handleSearchPeople(true, searchName));
}

// Load up to previously loaded page when returning.
let newNextPage = sessionStorage.getItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
if (newNextPage && nextPage) {
let startingPage = nextPage,
pagesToLoad = Number.parseInt(newNextPage) - nextPage;

if (pagesToLoad) {
handlePromiseError(
Promise.all(
Array.from({ length: pagesToLoad }).map((_, i) => {
return getAllPeople({ withHidden: true, page: startingPage + i });
}),
).then((pages) => {
for (const page of pages) {
people = people.concat(page.people);
}
nextPage = pages.at(-1)?.hasNextPage ? startingPage + pagesToLoad : null;
restoreScrollPosition(); // wait until extra pages are loaded
}),
);
} else {
restoreScrollPosition();
}
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
}

return websocketEvents.on('on_person_thumbnail', (personId: string) => {
for (const person of people) {
if (person.id === personId) {
Expand Down Expand Up @@ -311,6 +363,7 @@
<UserPageLayout
title={$t('people')}
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
bind:scrollSlot
>
<svelte:fragment slot="buttons">
{#if people.length > 0}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
Expand All @@ -25,7 +25,7 @@
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute, QueryParameter } from '$lib/constants';
import { AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
Expand Down Expand Up @@ -126,6 +126,14 @@
});
});

beforeNavigate(({ to }) => {
// Forget scroll position from people page if going somewhere else.
if (to && !to.url.pathname.startsWith(AppRoute.PEOPLE)) {
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
}
});

const handleEscape = async () => {
if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) {
return;
Expand Down Expand Up @@ -187,7 +195,7 @@
type: NotificationType.Info,
});

await goto(previousRoute, { replaceState: true });
await goto(previousRoute);
} catch (error) {
handleError(error, $t('errors.unable_to_hide_person'));
}
Expand Down