fix(participants): joindre participation→profil à travers les deux espaces d'id
Une Participation stocke son user comme principal stable `urn:festipod:user:<clé>`, alors qu'un UserProfile a pour `id` son NURI `did🆖`. La jointure brute `partUserIds.includes(u.id)` ne matchait donc jamais en mode connecté → chaque participant s'affichait « inconnu ». - `resolveParticipantUser` : match direct (espace seed demo) puis, à défaut, match sur `normalizeIdentifier(username)` après retrait du préfixe principal. - `USER_PRINCIPAL_PREFIX` : source unique du préfixe, partagée par l'écriture (`currentUserId`) et la lecture, pour qu'elles ne divergent pas. - EventDetailScreen : filtrer soi-même sur `currentUser?.id` (id de profil, même espace que `p.id`) et non sur `currentUserId` (principal). Aussi : épingle `packageManager` pnpm (l'install passe par pnpm, cf. rule_bun-first) — le runtime/test/build restent Bun. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user