diff --git a/apps/web/src/lib/contrail/events-load-more.ts b/apps/web/src/lib/contrail/events-load-more.ts index 2410c8a..3cd24a6 100644 --- a/apps/web/src/lib/contrail/events-load-more.ts +++ b/apps/web/src/lib/contrail/events-load-more.ts @@ -11,6 +11,7 @@ import { import { runEventSearchPage, searchBackendFromEnv } from '$lib/search/server/query'; import { SEARCH_PAGE_SIZE } from '$lib/search/constants'; import { orQueryFromSlug } from '$lib/topics'; +import { hasEnded } from '$lib/past-events'; import { decodeCursor, nextCursor, type CursorArgs, type CursorEnvelope, type CursorQuery } from './cursor'; const PAGE_SIZE = 20; @@ -110,16 +111,21 @@ const REGISTRY: Record = { 'past-events': async (_env, client, { args, raw }) => { if (!args?.actor || !isActorIdentifier(args.actor)) return EMPTY; + const asOf = now(); const response = await listAuthoredEventsFromContrail(client, { actor: args.actor as ActorIdentifier, - startsAtMax: now(), + startsAtMax: asOf, sort: 'startsAt', order: 'desc', profiles: true, limit: PAGE_SIZE, cursor: raw }); - return toResult('past-events', args, response); + // Page 1 narrows the same way, with the same shared predicate: startsAtMax + // still admits an event that began earlier and is still running, and an + // ongoing event must not be hidden on page 1 only to resurface on page 2. + const result = toResult('past-events', args, response); + return { ...result, events: result.events.filter((e) => hasEnded(e, asOf)) }; }, topic: async (_env, client, { args, raw }) => { diff --git a/apps/web/src/lib/past-events.ts b/apps/web/src/lib/past-events.ts new file mode 100644 index 0000000..8b22a30 --- /dev/null +++ b/apps/web/src/lib/past-events.ts @@ -0,0 +1,19 @@ +import type { FlatEventRecord } from '@atmo-dev/events-ui'; + +/** + * Has this event finished as of `asOf` (an ISO instant)? + * + * The "past events" list bounds its D1 query on startsAt, which still admits an + * event that began earlier and is STILL RUNNING. Page 1 and the load-more + * resumer both narrow their results with this predicate, so an ongoing event + * cannot be filtered off page 1 and then reappear on page 2. + * + * An event with no endsAt is treated as over once it has started, matching how + * the rest of the app approximates duration it was never given. + */ +export function hasEnded( + event: Pick, + asOf: string +): boolean { + return new Date(event.endsAt || event.startsAt) < new Date(asOf); +} diff --git a/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts b/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts index 44fe029..301aa66 100644 --- a/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts +++ b/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts @@ -6,6 +6,7 @@ import { listAuthoredEventsFromContrail } from '$lib/contrail'; import { nextCursor, rawForQuery } from '$lib/contrail/cursor'; +import { hasEnded } from '$lib/past-events'; import { isActorIdentifier } from '@atcute/lexicons/syntax'; import { error } from '@sveltejs/kit'; @@ -37,9 +38,12 @@ export async function load({ params, url, platform }) { }) ]); - const nowDate = new Date(now); - const events = (response ? flattenEventRecords(response.records) : []).filter( - (e) => new Date(e.endsAt || e.startsAt) < nowDate + // Narrow to events that have actually ENDED — startsAtMax still admits one + // that began earlier and is still running. The load-more resumer applies the + // same shared predicate, so an ongoing event can't be dropped here only to + // reappear on page 2. + const events = (response ? flattenEventRecords(response.records) : []).filter((e) => + hasEnded(e, now) ); return { diff --git a/apps/web/src/routes/(app)/p/[actor]/past-events/+page.svelte b/apps/web/src/routes/(app)/p/[actor]/past-events/+page.svelte index a2b76e1..9e9ec25 100644 --- a/apps/web/src/routes/(app)/p/[actor]/past-events/+page.svelte +++ b/apps/web/src/routes/(app)/p/[actor]/past-events/+page.svelte @@ -38,7 +38,11 @@ {hostName} - {#if (data.events?.length ?? 0) > 0} + + {#if (data.events?.length ?? 0) > 0 || data.cursor}