Ng eventually #1

Open
Sylvain wants to merge 110 commits from ng-eventually into main
4 changed files with 60 additions and 6 deletions
Showing only changes of commit c5e627c5fc - Show all commits
@@ -7,3 +7,4 @@
- TOUCHED src/shared/context/FestipodDataContext.tsx @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/auth/screens/AccessGateScreen.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/shared/context/AccountContext.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/event/screens/EventDetailScreen.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
+2 -1
View File
@@ -64,5 +64,6 @@
"onlyBuiltDependencies": [
"bun"
]
}
},
"packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402"
}
@@ -8,7 +8,7 @@ export function EventDetailScreen() {
const { eventId } = useParams();
const {
getEvent,
currentUserId,
currentUser,
isParticipating,
joinEvent,
leaveEvent,
@@ -30,7 +30,13 @@ export function EventDetailScreen() {
}));
const isOwner = true;
const knownParticipants = participants.filter(p => p.id !== currentUserId);
// Preview list shows the OTHER participants (deliberate — the total is in the
// header count; the full list at "Voir tous les participants" shows everyone).
// Compare on the PROFILE id: `currentUser.id` is the resolved profile NURI, the
// same space as `p.id` — whereas `currentUserId` is the `urn:festipod:user:`
// principal, which never equals a profile id in connected mode (so the old
// `p.id !== currentUserId` failed to drop self, leaking it in as "1 unknown").
const knownParticipants = participants.filter(p => p.id !== currentUser?.id);
const handleToggleJoin = () => {
if (!eventId) return;
+49 -3
View File
@@ -99,6 +99,42 @@ function nextId(prefix: string): string {
return `${prefix}-${++idCounter}`;
}
// The STABLE user-principal prefix. A Participation stores its user (`fp:user`) as
// this principal derived from the login identifier — `urn:festipod:user:<key>` —
// NOT as the UserProfile's `did:ng:` document NURI. `currentUserId` is minted with
// the SAME prefix below, so a participation keyed on it stays consistent with the
// identity the SDK/caps derive. The single source of truth for the prefix, shared
// by the WRITE (currentUserId) and the READ (resolveParticipantUser) so they never
// drift.
const USER_PRINCIPAL_PREFIX = 'urn:festipod:user:';
/**
* Resolve a Participation's `fp:user` to its UserProfile across the TWO id spaces
* that meet at this join (the root cause of the "unknown participant" bug):
* • a Participation stores `urn:festipod:user:<normalized-identifier>` (the stable
* principal = `currentUserId`), while
* • a UserProfile's `id` is its `did:ng:` document NURI — never that principal.
* The bridge is the NORMALIZED IDENTIFIER, which equals `normalizeIdentifier(username)`
* for the matching profile (the exact equality `currentUser` resolution already uses).
* So: strip the principal prefix off the participation's userId, and compare the
* remainder to `normalizeIdentifier(profile.username)`. In demo/local mode both sides
* are the bare seed id (`user-1`), matched directly by `u.id === userId` — which is
* why the direct match is tried FIRST (the seed username `@mariedupont` would not
* normalize to `user-1`). A per-deposit materializer uid (`mint...`, e.g.
* `mrktnoke-rzd699dk`) is a THIRD, unrelated space: it identifies an inbox deposit
* for the count, never a user — it does not participate in this join.
*/
function resolveParticipantUser(userId: string, users: FpUserData[]): FpUserData | undefined {
// 1) Direct id match — demo/local seed space (`user-1`), or any coincident space.
const direct = users.find(u => u.id === userId);
if (direct) return direct;
// 2) NG principal space: `urn:festipod:user:<key>` → match on normalized username.
const key = userId.startsWith(USER_PRINCIPAL_PREFIX)
? userId.slice(USER_PRINCIPAL_PREFIX.length)
: userId;
return users.find(u => u.username && normalizeIdentifier(u.username) === key);
}
// NG shape → app type mapping lives in `../data/shapeAdapters` (domain adapters
// over the SDK's `watchShape` subjects).
@@ -118,8 +154,18 @@ function buildQueries(
const getUser = (id: string) => users.find(u => u.id === id);
const getEventParticipants = (eventId: string) => {
const partUserIds = participations.filter(p => p.eventId === eventId).map(p => p.userId);
return users.filter(u => partUserIds.includes(u.id));
// Resolve each of the event's participations to its UserProfile across the two
// id spaces (participation principal vs profile NURI) — see
// `resolveParticipantUser`. A raw `partUserIds.includes(u.id)` join never
// matched in connected mode (principal ≠ NURI) → every participant rendered as
// "unknown". Deduped, returned in `users` order to match the prior contract.
const eventParts = participations.filter(p => p.eventId === eventId);
const resolvedIds = new Set<string>();
for (const p of eventParts) {
const u = resolveParticipantUser(p.userId, users);
if (u) resolvedIds.add(u.id);
}
return users.filter(u => resolvedIds.has(u.id));
};
const getUserEvents = (userId: string) => {
@@ -462,7 +508,7 @@ function useNgData(): FestipodDataContextValue {
// participations keyed on it are consistent with reads and isolation. Falls
// back to the read profile's IRI only when there is no login (dev/demo).
const currentUserId =
(identifier ? `urn:festipod:user:${normalizeIdentifier(identifier)}` : (currentUser?.id || ''));
(identifier ? `${USER_PRINCIPAL_PREFIX}${normalizeIdentifier(identifier)}` : (currentUser?.id || ''));
// Identity-first log prefix, reused by every DATA log below (including the
// closures defined earlier in this function body — they only execute after
// this render has finished, by which point `logPrefix` is initialized).