From 16e1644ec66ac6a3ef44a70fdf6e7ae628af3850 Mon Sep 17 00:00:00 2001 From: lkjsxc Date: Sun, 24 May 2026 18:59:19 +0900 Subject: [PATCH] Fix Home nav scroll-to-top on current timeline --- web/src/lib/components/Timeline.svelte | 12 +++++- web/src/lib/timelines/ScrollToTop.ts | 35 +++++++++++++++++ web/src/routes/(app)/Header.svelte | 50 +++++++++++++++++++----- web/src/routes/(app)/home/+page.svelte | 2 +- web/src/routes/(app)/public/+page.svelte | 2 +- 5 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 web/src/lib/timelines/ScrollToTop.ts diff --git a/web/src/lib/components/Timeline.svelte b/web/src/lib/components/Timeline.svelte index d1583734..fa83310e 100644 --- a/web/src/lib/components/Timeline.svelte +++ b/web/src/lib/components/Timeline.svelte @@ -21,6 +21,7 @@ import { MouseButton } from '$lib/DomHelper'; import { scrollY } from 'svelte/reactivity/window'; import { isVisibleNotification } from '$lib/preferences/NotificationVisibility.svelte'; + import { onTimelineScrollToTop } from '$lib/timelines/ScrollToTop'; interface Props { timeline: NewTimeline; @@ -29,6 +30,7 @@ createdAtFormat?: 'auto' | 'time'; full?: boolean; canTransition?: boolean; + scrollToTopTarget?: string; } let { @@ -37,7 +39,8 @@ showLoading = true, createdAtFormat = 'auto', full = false, - canTransition = true + canTransition = true, + scrollToTopTarget }: Props = $props(); let isTop = $state(true); @@ -171,6 +174,11 @@ }; onMount(() => { + const disposeTimelineScrollToTop = + scrollToTopTarget === undefined + ? undefined + : onTimelineScrollToTop(scrollToTopTarget, scrollToTop); + // Workaround for scroll position if (!timeline.latest) { setTimeout(() => { @@ -180,6 +188,8 @@ newer(); }, 1000); } + + return disposeTimelineScrollToTop; }); $effect(() => { diff --git a/web/src/lib/timelines/ScrollToTop.ts b/web/src/lib/timelines/ScrollToTop.ts new file mode 100644 index 00000000..ef7edc96 --- /dev/null +++ b/web/src/lib/timelines/ScrollToTop.ts @@ -0,0 +1,35 @@ +const timelineScrollToTopEvent = 'nostter:timeline-scroll-to-top'; + +type TimelineScrollToTopDetail = { + target: string; +}; + +export function requestTimelineScrollToTop(target: string): void { + if (typeof window === 'undefined') { + return; + } + window.dispatchEvent( + new CustomEvent(timelineScrollToTopEvent, { + detail: { target } + }) + ); +} + +export function onTimelineScrollToTop( + target: string, + handler: () => void | Promise +): () => void { + if (typeof window === 'undefined') { + return () => {}; + } + const listener = (event: Event) => { + const detail = event instanceof CustomEvent ? event.detail : undefined; + if ((detail as TimelineScrollToTopDetail | undefined)?.target !== target) { + return; + } + + void handler(); + }; + window.addEventListener(timelineScrollToTopEvent, listener); + return () => window.removeEventListener(timelineScrollToTopEvent, listener); +} diff --git a/web/src/routes/(app)/Header.svelte b/web/src/routes/(app)/Header.svelte index beaee9e8..7d50719b 100644 --- a/web/src/routes/(app)/Header.svelte +++ b/web/src/routes/(app)/Header.svelte @@ -15,6 +15,7 @@ import { nip19 } from 'nostr-tools'; import { _ } from 'svelte-i18n'; import { goto } from '$app/navigation'; + import { page } from '$app/state'; import { followees, pubkey, rom } from '$lib/stores/Author'; import { openNoteDialog } from '$lib/stores/NoteDialog'; import { lastReadAt, notifiedEventItems } from '$lib/author/Notifications'; @@ -22,6 +23,8 @@ import NostterLogoIcon from '$lib/components/logo/NostterLogoIcon.svelte'; import { createDropdownMenu, melt } from '@melt-ui/svelte'; import { isVisibleNotification } from '$lib/preferences/NotificationVisibility.svelte'; + import { MouseButton } from '$lib/DomHelper'; + import { requestTimelineScrollToTop } from '$lib/timelines/ScrollToTop'; const { elements: { menu, item, trigger, overlay } @@ -31,6 +34,39 @@ $openNoteDialog = !$openNoteDialog; } + function requestTimelineScrollToTopForCurrentLink(event: MouseEvent, link: string): boolean { + if ( + event.button !== MouseButton.Left || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey || + page.url.pathname !== link + ) { + return false; + } + + event.preventDefault(); + requestTimelineScrollToTop(link); + return true; + } + + function onClickHomeLink(event: MouseEvent): void { + requestTimelineScrollToTopForCurrentLink(event, homeLink); + } + + function onClickPublicLink(event: MouseEvent): void { + requestTimelineScrollToTopForCurrentLink(event, '/public'); + } + + async function onClickPublicMenuItem(event: MouseEvent): Promise { + if (requestTimelineScrollToTopForCurrentLink(event, '/public')) { + return; + } + + await goto('/public'); + } + let homeLink = $derived( $followees.filter((x) => x !== $pubkey).length > 0 ? '/home' : '/public' ); @@ -57,13 +93,13 @@