From fbb1f6f61baa9e5ac28739172645814912710319 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Mon, 12 Oct 2026 17:00:42 +0200 Subject: [PATCH] =?UTF-8?q?Open=20another=20user's=20event=20from=20D?= =?UTF-8?q?=C3=A9couvrir:=20detail,=20meeting=20points,=20counts=20and=20j?= =?UTF-8?q?oin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping a catalog event used to show "Événement non trouvé" because the detail only searched the user's own events. An event is now read by URI, its meeting points come from the catalog, and each meeting point's count comes from its attendees collection (event count = sum); joining a foreign meeting point goes through the existing outbox flow and the counts move at once and survive a reload. Foreign records are read anonymously by direct HTTP: the session token only goes to the session's own origin, each read is bounded and independent, and one malformed catalog item, record or collection no longer takes the whole read down. Own counts were always 0 before, because the pod never embeds apods:attendees; they are now read the same way. Catalog-only cards show no count instead of a misleading zero. --- .../event/screens/EventDetailScreen.tsx | 16 +- src/modules/event/screens/EventsScreen.tsx | 38 ++- .../components/sketchy/EventMeetingPoints.tsx | 8 + src/shared/context/FestipodDataContext.tsx | 2 +- src/shared/context/festipodDataBase.ts | 41 ++- src/shared/context/refineData.tsx | 257 +++++++++++++++--- src/shared/context/refineDerive.test.ts | 90 +++++- src/shared/context/refineDerive.ts | 104 +++++-- src/shared/context/usePublicRecords.test.ts | 37 +++ src/shared/context/usePublicRecords.ts | 100 +++++++ src/shared/data/actor.ts | 16 +- src/shared/data/attendees.test.ts | 54 ++++ src/shared/data/attendees.ts | 39 +++ src/shared/data/catalog.test.ts | 90 +++++- src/shared/data/catalog.ts | 143 ++++++++-- src/shared/data/publicRead.test.ts | 117 ++++++++ src/shared/data/publicRead.ts | 120 ++++++++ src/shared/data/types.ts | 6 + 18 files changed, 1179 insertions(+), 99 deletions(-) create mode 100644 src/shared/context/usePublicRecords.test.ts create mode 100644 src/shared/context/usePublicRecords.ts create mode 100644 src/shared/data/attendees.test.ts create mode 100644 src/shared/data/attendees.ts create mode 100644 src/shared/data/publicRead.test.ts create mode 100644 src/shared/data/publicRead.ts diff --git a/src/modules/event/screens/EventDetailScreen.tsx b/src/modules/event/screens/EventDetailScreen.tsx index 1c48374..7dd1793 100644 --- a/src/modules/event/screens/EventDetailScreen.tsx +++ b/src/modules/event/screens/EventDetailScreen.tsx @@ -1,3 +1,4 @@ +import { useLayoutEffect } from 'react'; import { ArrowLeft } from 'lucide-react'; import { Button, Avatar, EventCover, EventMeetingPoints, showToast, Text, type MeetingPointData } from '../../../shared/components/sketchy'; import { useFestipodData } from '../../../shared/context/FestipodDataContext'; @@ -15,8 +16,19 @@ export function EventDetailScreen() { getEventParticipants, getEventMeetingPoints, getEventOwnership, + openEvent, + getEventReadState, } = useFestipodData(); + // ANOTHER USER'S EVENT IS NOT IN THE OWN LIST. Découvrir links to events from + // every pod; the data layer reads one it does not hold only when asked. Asked + // in a LAYOUT effect so the request lands before the first paint — the + // "not found" state must never flash for an event that is about to load. + useLayoutEffect(() => { + if (eventId) openEvent(eventId); + }, [eventId, openEvent]); + const readState = eventId ? getEventReadState(eventId) : 'unavailable'; + const event = eventId ? getEvent(eventId) : undefined; const participants = eventId ? getEventParticipants(eventId) : []; const meetingPointsRaw = eventId ? getEventMeetingPoints(eventId) : []; @@ -27,6 +39,8 @@ export function EventDetailScreen() { when: mp.time, duration: '~60 min', lieu: mp.location, + // Derived by the data layer (absent where it cannot be counted per point). + participantCount: mp.participantCount, })); // SIGNING UP IS PER MEETING POINT — the product model (on s'inscrit à un @@ -85,7 +99,7 @@ export function EventDetailScreen() { Événement
- Événement non trouvé + {readState === 'loading' ? "Chargement de l'événement…" : 'Événement non trouvé'}
); diff --git a/src/modules/event/screens/EventsScreen.tsx b/src/modules/event/screens/EventsScreen.tsx index d32cc18..2f7dd0e 100644 --- a/src/modules/event/screens/EventsScreen.tsx +++ b/src/modules/event/screens/EventsScreen.tsx @@ -16,13 +16,16 @@ const EVENT_COLORS = ['#E8590C', '#2B6CB0', '#9C4DC7', '#38A169', '#D69E2E']; function EventCard({ event, + participantCount, onClick, }: { - event: { id: string; title: string; date: string; location: string; distance?: number; participantCount: number }; + event: { id: string; title: string; date: string; location: string; distance?: number }; + /** `undefined` when nobody counted it (a catalog entry) — then no count is shown, never a made-up 0. */ + participantCount: number | undefined; onClick: () => void; }) { const color = EVENT_COLORS[Number(event.id.replace(/\D/g, '') || 0) % EVENT_COLORS.length] ?? '#E8590C'; - const people = PEOPLE.slice(0, Math.min(5, Math.max(1, event.participantCount))); + const people = PEOPLE.slice(0, Math.min(5, Math.max(1, participantCount ?? 0))); return (
{event.date} · {event.location}{event.distance != null && ` · ${event.distance} km`}
-
-
- - {event.participantCount} inscrits + {participantCount !== undefined && ( +
+
+ + {participantCount} inscrits +
-
+ )}
); @@ -51,7 +56,7 @@ function EventCard({ export function EventsScreen() { const navigate = useNavigate(); - const { events: ownEvents } = useFestipodData(); + const { events: ownEvents, getEvent } = useFestipodData(); const events = useDiscoverEvents(ownEvents); return ( @@ -78,9 +83,20 @@ export function EventsScreen() { Aucun événement à afficher )} - {events.map(event => ( - navigate(eventPath(event.id))} /> - ))} + {events.map(event => { + // An event the data layer READ (own, or another user's opened by URI) + // carries a derived count; a catalog entry nobody opened carries none — + // its card shows no count rather than a misleading "0 inscrits". + const held = getEvent(event.id); + return ( + navigate(eventPath(event.id))} + /> + ); + })} diff --git a/src/shared/components/sketchy/EventMeetingPoints.tsx b/src/shared/components/sketchy/EventMeetingPoints.tsx index ae5420e..4fee460 100644 --- a/src/shared/components/sketchy/EventMeetingPoints.tsx +++ b/src/shared/components/sketchy/EventMeetingPoints.tsx @@ -7,6 +7,8 @@ export interface MeetingPointData { when: string; duration: string; lieu: string; + /** How many signed up to this point — shown when the caller knows it, omitted otherwise. */ + participantCount?: number; } interface Props { @@ -56,6 +58,12 @@ export function EventMeetingPoints({ points, joinedIds, onToggle, expanded = fal 🕒 {p.when} · {p.duration}
📍 {p.lieu} + {p.participantCount !== undefined && ( + <> +
+ 👥 {p.participantCount} inscrit{p.participantCount > 1 ? 's' : ''} + + )}