feat(web): persist people page scroll position when clicking back on person page

This commit is contained in:
Calum Dingwall 2024-07-26 17:20:35 -06:00
parent a3799b3053
commit 1eed0e14f5
4 changed files with 74 additions and 6 deletions

View File

@ -11,6 +11,8 @@
export let scrollbar = true; export let scrollbar = true;
export let admin = false; export let admin = false;
export let scrollSlot: HTMLDivElement;
$: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden'; $: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden';
$: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full'; $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
</script> </script>
@ -49,7 +51,10 @@
</div> </div>
{/if} {/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 /> <slot />
</div> </div>
</section> </section>

View File

@ -78,6 +78,11 @@ export enum QueryParameter {
PAGE = 'page', PAGE = 'page',
} }
export enum SessionStorageKey {
INFINITE_SCROLL_PAGE = 'infiniteScrollPage',
SCROLL_POSITION = 'scrollPosition',
}
export enum OpenSettingQueryParameterValue { export enum OpenSettingQueryParameterValue {
OAUTH = 'oauth', OAUTH = 'oauth',
JOB = 'job', JOB = 'job',

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { beforeNavigate, goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { focusTrap } from '$lib/actions/focus-trap'; import { focusTrap } from '$lib/actions/focus-trap';
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
@ -17,7 +17,7 @@
notificationController, notificationController,
NotificationType, NotificationType,
} from '$lib/components/shared-components/notification/notification'; } 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 { locale } from '$lib/stores/preferences.store';
import { websocketEvents } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
@ -61,12 +61,61 @@
let changeNameInputEl: HTMLInputElement | null; let changeNameInputEl: HTMLInputElement | null;
let innerHeight: number; 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: parseFloat(newScroll),
behavior: 'instant',
});
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
};
onMount(() => { onMount(() => {
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE); const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
if (getSearchedPeople) { if (getSearchedPeople) {
searchName = getSearchedPeople; searchName = getSearchedPeople;
handlePromiseError(handleSearchPeople(true, searchName)); 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 = parseInt(newNextPage) - nextPage;
if (pagesToLoad) {
handlePromiseError(
Promise.all(
Array(pagesToLoad).map((_, i) => {
return getAllPeople({ withHidden: true, page: startingPage + i });
}),
).then((pages) => {
pages.forEach((page) => {
people = people.concat(page.people);
});
nextPage = pages[pages.length - 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) => { return websocketEvents.on('on_person_thumbnail', (personId: string) => {
for (const person of people) { for (const person of people) {
if (person.id === personId) { if (person.id === personId) {
@ -311,6 +360,7 @@
<UserPageLayout <UserPageLayout
title={$t('people')} title={$t('people')}
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`} description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
bind:scrollSlot
> >
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
{#if people.length > 0} {#if people.length > 0}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
@ -25,7 +25,7 @@
NotificationType, NotificationType,
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } 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 { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store'; import { AssetStore } from '$lib/stores/assets.store';
@ -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 () => { const handleEscape = async () => {
if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) { if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) {
return; return;
@ -187,7 +195,7 @@
type: NotificationType.Info, type: NotificationType.Info,
}); });
await goto(previousRoute, { replaceState: true }); await goto(previousRoute);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_hide_person')); handleError(error, $t('errors.unable_to_hide_person'));
} }