atmo.rsvp
1import {
2 buildAttendee,
3 flattenEventRecord,
4 flattenEventRecords,
5 getServerClient,
6 listDiscoverableEventsFromContrail,
7 listEventRecordsFromContrail,
8 type ActivityCluster
9} from '$lib/contrail';
10import { getSpacesClient } from '$lib/spaces/server/client';
11import { spacesAvailable } from '$lib/spaces/config';
12import type { PageServerLoad } from './$types';
13
14const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
15const ACTIVITY_FETCH_LIMIT = 100;
16const ACTIVITY_DISPLAY_LIMIT = 10;
17
18export const load: PageServerLoad = async ({ locals, platform }) => {
19 const publicClient = getServerClient(platform!.env.DB);
20 const nowIso = new Date().toISOString();
21
22 const myEventsPromise = (async () => {
23 if (!locals.did) return { upcoming: [], past: [] };
24
25 const client =
26 locals.client && spacesAvailable()
27 ? getSpacesClient(locals.client, platform!.env.DB)
28 : publicClient;
29
30 const cutoff = new Date(Date.now() - SEVEN_DAYS_MS);
31 const cutoffIso = cutoff.toISOString();
32
33 const [rsvpResponse, hostingResponse] = await Promise.all([
34 client.get('rsvp.atmo.rsvp.listRecords', {
35 params: { actor: locals.did, hydrateEvent: true, limit: 100 }
36 }),
37 listEventRecordsFromContrail(client, {
38 actor: locals.did,
39 startsAtMin: cutoffIso,
40 sort: 'startsAt',
41 order: 'asc',
42 limit: 100
43 })
44 ]);
45
46 const rsvpEvents = (rsvpResponse.ok ? (rsvpResponse.data.records ?? []) : [])
47 .filter((r) => {
48 const status = r.record?.status;
49 return status?.endsWith('#going') || status?.endsWith('#interested');
50 })
51 .flatMap((r) => {
52 if (!r.event) return [];
53 const flat = flattenEventRecord(r.event);
54 return flat ? [flat] : [];
55 })
56 .filter((e) => new Date(e.endsAt || e.startsAt) >= cutoff);
57
58 const hostingEvents = hostingResponse ? flattenEventRecords(hostingResponse.records) : [];
59
60 const seen = new Set<string>();
61 const all = [...rsvpEvents, ...hostingEvents].filter((e) => {
62 if (seen.has(e.uri)) return false;
63 seen.add(e.uri);
64 return true;
65 });
66
67 const nowMs = Date.now();
68 const upcoming = all
69 .filter((e) => new Date(e.endsAt || e.startsAt).getTime() >= nowMs)
70 .sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
71 const past = all
72 .filter((e) => new Date(e.endsAt || e.startsAt).getTime() < nowMs)
73 .sort((a, b) => new Date(b.startsAt).getTime() - new Date(a.startsAt).getTime());
74
75 return { upcoming, past };
76 })();
77
78 const globalPromise = listDiscoverableEventsFromContrail(publicClient, {
79 startsAtMin: nowIso,
80 rsvpsGoingCountMin: 2,
81 hydrateRsvps: 5,
82 sort: 'startsAt',
83 order: 'asc',
84 limit: 20
85 });
86
87 const recentActivityPromise = (async (): Promise<ActivityCluster[]> => {
88 const response = await publicClient.get('rsvp.atmo.rsvp.listRecords', {
89 params: {
90 hydrateEvent: true,
91 profiles: true,
92 limit: ACTIVITY_FETCH_LIMIT
93 }
94 });
95 if (!response.ok) return [];
96
97 const records = response.data.records ?? [];
98 const profiles = response.data.profiles ?? [];
99 const nowMs = Date.now();
100 const clusters = new Map<string, ActivityCluster>();
101
102 for (const r of records) {
103 const status = r.record?.status;
104 const isGoing = status?.endsWith('#going');
105 const isInterested = status?.endsWith('#interested');
106 if (!isGoing && !isInterested) continue;
107
108 if (!r.event) continue;
109 const flatEvent = flattenEventRecord(r.event);
110 if (!flatEvent) continue;
111
112 const eventEndMs = new Date(flatEvent.endsAt || flatEvent.startsAt).getTime();
113 if (eventEndMs < nowMs) continue;
114
115 const attendee = buildAttendee(r.did, isGoing ? 'going' : 'interested', profiles);
116
117 let cluster = clusters.get(flatEvent.uri);
118 if (!cluster) {
119 cluster = { event: flatEvent, attendees: [], latestTimeUs: r.time_us };
120 clusters.set(flatEvent.uri, cluster);
121 }
122 cluster.attendees.push(attendee);
123 if (r.time_us > cluster.latestTimeUs) cluster.latestTimeUs = r.time_us;
124 }
125
126 return Array.from(clusters.values())
127 .sort((a, b) => b.latestTimeUs - a.latestTimeUs)
128 .slice(0, ACTIVITY_DISPLAY_LIMIT);
129 })();
130
131 const [myEvents, response, recentActivity] = await Promise.all([
132 myEventsPromise,
133 globalPromise,
134 recentActivityPromise
135 ]);
136
137 if (!response) {
138 return {
139 events: [],
140 handles: {},
141 myUpcoming: myEvents.upcoming,
142 myPast: myEvents.past,
143 recentActivity
144 };
145 }
146
147 const handles: Record<string, string> = {};
148 for (const p of response.profiles ?? []) {
149 if (p.handle) handles[p.did] = p.handle;
150 }
151
152 return {
153 events: flattenEventRecords(response.records),
154 handles,
155 myUpcoming: myEvents.upcoming,
156 myPast: myEvents.past,
157 recentActivity
158 };
159};