// The LIST half of sub-task 5a Task 9 (the count button lives in `MeetingWindow`'s header). // // THE CLIENT CANNOT NAME ANYONE FROM `attendeeId` ALONE - it is an opaque Chime id. What CAN name a // participant is `externalUserId`, which `ChimeCallSession` now carries (see `RemoteMeetingParticipant`) // and which - per the video contract - IS the participant's stable FHIR reference (`Practitioner/abc`, // `Patient/xyz`). This component resolves that reference to a name. // // NEVER RENDER THE RAW REFERENCE. `Practitioner/9f2c-...` means nothing to a patient and leaks an // internal id into a room a related person may be sitting in. The fallback RULES live in // `participantIdentity.ts` (dependency-free, so they are unit-testable in plain node without pulling in // `environment.ts`'s module-scope `window` read); this file only does the fetching. // // Participant identity is PHI-adjacent: resolved names live in this component's own React state only - // no localStorage/sessionStorage - and are re-fetched fresh on every mount. The fetch-and-cache logic // itself lives in `useParticipantNames` so the main stage's name card and this list // resolve a participant's identity through ONE implementation instead of two. import { useState } from 'react'; import type { RemoteMeetingParticipant } from '../../services/video/ChimeCallSession'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'; import { Z_POPOVER_DIALOG_CLASS } from '../../lib/stackingLayers'; import { initialsOf, resolveParticipantLabel } from './participantIdentity'; import { useParticipantNames } from './useParticipantNames'; export interface MeetingParticipantsPanelProps { participants: RemoteMeetingParticipant[]; /** Remove is offered to the host only, and is ABSENT rather than disabled for everyone else - the * same treatment the clinical note tab gets for a non-host. Defaults to false so an omitted prop * cannot hand out a host control. */ isHost?: boolean; /** Takes the participant's FHIR reference (Chime's `externalUserId`). Absent means no control is * offered, so a caller that forgets to wire it degrades to a read-only roster. */ onRemove?: (participantRef: string) => void | Promise; /** What the removal copy calls the meeting. A case conference is not a visit. */ meeting?: 'visit' | 'conference'; /** The viewer's own name for their row; without it the row reads "You". */ selfName?: string; } // What the reference says the person is; the name alone does not tell a patient from a clinician. const ROLE: Record = { Patient: 'Patient', Practitioner: 'Practitioner', RelatedPerson: 'Related person' }; const roleOf = (ref?: string) => (ref ? ROLE[ref.split('/')[0]] : undefined); const Avatar = ({ text }: { text: string }) => ( ); /** One row per remote participant and the viewer's own row last - Zoom's mobile pattern (Spencer's * ruling): a count alone tells you five other people are here, the list is where you find out who. * Laid out as practitioner#s-video-visit board 23. */ export const MeetingParticipantsPanel: React.FC = ({ participants, isHost = false, onRemove, meeting = 'visit', selfName, }) => { const names = useParticipantNames(participants); // Removal is irreversible for this session, so it takes a second press, in a dialog named for the // person (board 24). const [confirming, setConfirming] = useState<{ ref: string; label: string } | undefined>(); const canRemove = isHost && typeof onRemove === 'function'; return (

In this {meeting} · {participants.length + 1}

{participants.map((p) => { // `||` here too, for the same reason as inside `useParticipantNames`'s fetch: `names[...]` // may be an empty string ("fetched, no usable name") or simply absent (not yet fetched) - // both must read as "no name yet", and `??` would not catch the empty string. const resolvedName = (p.externalUserId ? names[p.externalUserId] : undefined) || undefined; const label = resolveParticipantLabel(p.externalUserId, resolvedName); // No reference, no control: the server keys removal on it and there is nothing to guess. const removable = canRemove && Boolean(p.externalUserId); const role = roleOf(p.externalUserId); return (
{label} {role && {role}} {removable && ( )}
); })}
{selfName || 'You'} {isHost && Hosting this {meeting}} {selfName && you}
{ if (!open) setConfirming(undefined); }}> {/* Opened from the roster popover, so above it; drawn in the call's dark palette (board 24). */} Remove {confirming?.label} from this {meeting}?

They are disconnected immediately, and they will not be able to rejoin — the 10-minute window that lets somebody back after a dropped call does not apply to a removal.

The {meeting} carries on for everyone else. Nothing changes about their place on the collaboration team.

); };