atmo.rsvp
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 159 lines 4.6 kB view raw
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};