From 6a3a02bc34b5ef6b461296001a40e2b566cbb8b5 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 5 Feb 2025 17:05:41 -0800 Subject: [PATCH] Handle scrolling on calendar --- src/app/requests.ts | 26 ++++- src/lib/components/Divider.svelte | 7 +- .../spaces/[relay]/calendar/+page.svelte | 100 +++++++++++++----- 3 files changed, 97 insertions(+), 36 deletions(-) diff --git a/src/app/requests.ts b/src/app/requests.ts index 7a5e7c7b..3d9bdaee 100644 --- a/src/app/requests.ts +++ b/src/app/requests.ts @@ -8,6 +8,7 @@ import { COMMENT, matchFilters, getTagValues, + getTagValue, } from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util" import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds" @@ -155,16 +156,25 @@ export const makeCalendarFeed = ({ onExhausted?: () => void initialEvents?: TrustedEvent[] }) => { - const events = writable(initialEvents) - + let exhaustedScrollers = 0 let backwardWindow = [now() - MONTH, now()] let forwardWindow = [now(), now() + MONTH] + const getStart = (event: TrustedEvent) => parseInt(getTagValue("start", event.tags) || "") + + const getEnd = (event: TrustedEvent) => parseInt(getTagValue("end", event.tags) || "") + + const events = writable(sortBy(getStart, initialEvents)) + const insertEvent = (event: TrustedEvent) => { + const start = getStart(event) + + if (isNaN(start) || isNaN(getEnd(event))) return + events.update($events => { for (let i = 0; i < $events.length; i++) { if ($events[i].id === event.id) return $events - if ($events[i].created_at < event.created_at) return insert(i, event, $events) + if (getStart($events[i]) > start) return insert(i, event, $events) } return [...$events, event] @@ -201,8 +211,6 @@ export const makeCalendarFeed = ({ const loadTimeframe = (since: number, until: number) => { const hashes = daysBetween(since, until).map(String) - console.log(since, until, hashes) - load({ relays, filters: [{kinds: [EVENT_TIME], "#D": hashes}], @@ -210,6 +218,12 @@ export const makeCalendarFeed = ({ }) } + const maybeExhausted = () => { + if (++exhaustedScrollers === 2) { + onExhausted?.() + } + } + const backwardScroller = createScroller({ element, reverse: true, @@ -222,6 +236,7 @@ export const makeCalendarFeed = ({ loadTimeframe(since, until) } else { backwardScroller.stop() + maybeExhausted() } }, }) @@ -237,6 +252,7 @@ export const makeCalendarFeed = ({ loadTimeframe(since, until) } else { forwardScroller.stop() + maybeExhausted() } }, }) diff --git a/src/lib/components/Divider.svelte b/src/lib/components/Divider.svelte index 8861dbcf..12fc59d4 100644 --- a/src/lib/components/Divider.svelte +++ b/src/lib/components/Divider.svelte @@ -1,15 +1,16 @@
-
+
{#if children}

{@render children?.()}

-
+
{/if}
diff --git a/src/routes/spaces/[relay]/calendar/+page.svelte b/src/routes/spaces/[relay]/calendar/+page.svelte index bb57bf06..5e3ca708 100644 --- a/src/routes/spaces/[relay]/calendar/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/+page.svelte @@ -3,10 +3,11 @@ import type {Readable} from "svelte/store" import {readable} from "svelte/store" import {page} from "$app/stores" - import {sortBy, now, last} from "@welshman/lib" + import {now, last} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" import {REACTION, DELETE, EVENT_TIME, getTagValue} from "@welshman/util" import {formatTimestampAsDate} from "@welshman/app" + import {fly} from "@lib/transition" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" import Spinner from "@lib/components/Spinner.svelte" @@ -24,8 +25,6 @@ const createEvent = () => pushModal(EventCreate, {url}) - const getEnd = (event: TrustedEvent) => parseInt(getTagValue("end", event.tags) || "") - const getStart = (event: TrustedEvent) => parseInt(getTagValue("start", event.tags) || "") let element: HTMLElement @@ -36,23 +35,59 @@ type Item = { event: TrustedEvent dateDisplay?: string + isFirstFutureEvent?: boolean } - const items = $derived( - sortBy(e => getStart(e), $events).reduce((r, event) => { - const end = getEnd(event) - const start = getStart(event) + const items = $derived.by(() => { + const todayDateDisplay = formatTimestampAsDate(now()) - if (isNaN(start) || isNaN(end)) return r + let haveISeenTheFuture = false + let prevDateDisplay: string - const prevDateDisplay = - r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined - const newDateDisplay = formatTimestampAsDate(start) + return $events.map(event => { + const newDateDisplay = formatTimestampAsDate(getStart(event)) const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay + const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now() + const isFirstFutureEvent = !haveISeenTheFuture && isFuture - return [...r, {event, dateDisplay}] - }, []), - ) + prevDateDisplay = newDateDisplay + haveISeenTheFuture = isFuture + + return {event, dateDisplay, isFirstFutureEvent} + }) + }) + + let previousScrollHeight = 0 + let prevFirstEventId = "" + let initialScrollDone = false + + $effect(() => { + if (initialScrollDone) { + // If new events are prepended, adjust the scroll position so that the viewport content remains anchored + if (prevFirstEventId && items[0].event.id !== prevFirstEventId) { + const newScrollHeight = element.scrollHeight + const delta = newScrollHeight - previousScrollHeight + + if (delta > 0) { + element.scrollTop += delta + } + } + } else if (items.length > 0) { + const {event} = items.find(({event}) => getStart(event) >= now()) || last(items) + const {offsetTop, clientHeight} = document.querySelector( + ".calendar-event-" + event.id, + ) as HTMLElement + + // On initial load, center the scroll container on today's date (or the next available event) + element.scrollTop = offsetTop - element.clientHeight / 2 + clientHeight / 2 + initialScrollDone = true + } + + if (items.length > 0) { + previousScrollHeight = element.scrollHeight + prevFirstEventId = items[0].event.id + } + }) onMount(() => { const feedFilters = [{kinds: [EVENT_TIME], "#h": [GENERAL]}] @@ -95,21 +130,30 @@ {/snippet}
- {#each items as { event, dateDisplay }, i (event.id)} - {#if dateDisplay} - {dateDisplay} - {/if} - - {/each} -

- - {#if loading} - Looking for events... - {:else if items.length === 0} - No events found. + {#each items as { event, dateDisplay, isFirstFutureEvent }, i (event.id)} +

+ {#if isFirstFutureEvent} +
+
+

Today

+
+
{/if} - -

+ {#if dateDisplay} + {dateDisplay} + {/if} + +
+ {/each} + {#if loading} +

+ Looking for events... +

+ {:else if items.length === 0} +

No events found.

+ {:else} +

That's all!

+ {/if}