import { type Cell, computed, Default, equals, handler, NAME, pattern, type PerSpace, type PerUser, safeDateNow, Stream, UI, type VNode, wish, Writable, } from "commonfabric"; /** * LIVE multi-user profile roster demo (CT-1648/CT-1650 end-to-end check). * * Unlike `shared-profile-roster` (which SNAPSHOTS each participant's name+avatar * at join time and renders others with a plain ``), this demo stores * each participant's live `#profile` cell and renders EVERY participant — not * just the current viewer — with the trusted `` bound to that * cross-space profile cell. That exercises the full stack in a genuine * multi-user setting: * - per-user profile spaces (CT-1650), * - cross-space profile READ materialization (CT-1667/1687), * - the badge's bio + pinned-count hover tooltip (CT-1648). * * If cross-space profile reads work across users, viewer A hovering viewer B's * badge sees B's bio + pinned-piece count. If they don't, this demo is exactly * the harness that surfaces the gap. */ export type ParticipantProfileCell = Cell< { name?: string; avatar?: string; bio?: string } >; export interface Participant { /** Live link to the contributor's profile cell — stable identity + live data. */ profile: ParticipantProfileCell; /** Display name, snapshotted at join time (durable label; survives even if the live cell is momentarily unresolved on first render). */ name: string; joinedAt: number; } export interface Roster { participants: Participant[] | Default<[]>; } export interface ViewerState { joined?: boolean; } const DEFAULT_ROSTER: Roster = { participants: [] }; const EMPTY_VIEWER: ViewerState = {}; type RosterCell = Writable>; type ViewerCell = Writable>; export type JoinEvent = Record; const join = handler((_event, { roster, viewer, profile, name }) => { const trimmed = (name ?? "").trim(); if (!trimmed) return; if (!profile) return; const participants = roster.key("participants"); const already = participants.get().some((p) => equals(p.profile, profile)); if (!already) { participants.push({ profile, name: trimmed, joinedAt: safeDateNow() }); } viewer.set({ joined: true }); }); export interface ProfileRosterLiveInput { roster?: PerSpace>; viewer?: PerUser>; } export interface ProfileRosterLiveOutput { [NAME]: string; [UI]: VNode; roster: PerSpace>; viewer: PerUser>; participantCount: number; join: Stream; } const headerLabel = { fontSize: "0.75rem", fontWeight: "600", textTransform: "uppercase", letterSpacing: "0.04em", color: "#6b7280", }; export default pattern( ({ roster, viewer }) => { const profileWish = wish<{ name?: string; avatar?: string }>({ query: "#profile", }); const profileNameWish = wish({ query: "#profileName" }); const myName = computed(() => profileNameWish.result ?? ""); const myProfile = profileWish.result; const participants = roster.participants; const participantCount = participants.length; const hasJoined = computed(() => viewer.joined === true); const joinLabel = computed(() => hasJoined ? "Joined" : "Join as " + (myName || "…") ); const boundJoin = join({ roster, viewer, profile: myProfile, name: myName, }); return { [NAME]: "Live profile roster", [UI]: ( You { /* `#profile` is a wish RESULT (not the raw profile piece), so a click would route to a non-piece id and fail to load. The self badge is non-navigable (same convention as profile-home's self badge); the participant badges below bind raw profile cells and DO navigate. */ } Participants ({participantCount}) {joinLabel} { /* EVERY participant rendered as a LIVE badge bound to their own cross-space profile cell — hover to see their bio + pinned count. */ } {participants.map((p) => ( ))} ), roster, viewer, participantCount, join: boundJoin, }; }, );