// Who else is in the meeting, and what to say about it - derived ONCE and shared by both call // surfaces. // // BOTH OF THIS MODULE'S REASONS FOR EXISTING CAME OUT OF THE FIRST REAL BROWSER CALL // (Spencer, 2026-08-02): // // 1. The patient left and the practitioner was told NOTHING. The presence line lived only on the // docked `EncounterVideoPane`; `FloatingCallSurface` had none - its own comment said "Task 13 // decides what, if anything, it should say", and Task 13 never decided. The floating surface is // exactly where a practitioner ends up when they navigate away from the encounter, so the one // surface that could go silent was the one most likely to be on screen. // // 2. "Waiting for the patient to join..." is the WRONG SENTENCE for somebody who has left. Those are // different events - not arrived yet vs hung up - and they were indistinguishable because the // observable state is identical: empty roster, no remote tiles. `everRemote` is the only thing // that separates them, which is why `ChimeCallSession` now tracks it. // // Pure on purpose: no React, no SDK. Two surfaces asking the same question must not each answer it - // that divergence IS defect 1. import type { CallPhase } from './callMachine'; export type RoomPresence = 'occupied' | 'never-arrived' | 'departed'; export interface RoomFacts { phase: CallPhase; /** Everyone else on the roster. The PRIMARY signal - an audio-only participant has no tile. */ remoteAttendees: string[]; /** A weaker second witness (Task 9): presence tracking is best-effort, and a remote tile only * exists on a positive `localTile === false`, so it can reduce a false "waiting" but never * create a false "not waiting". */ remoteTiles: number[]; /** Has anybody else EVER been in this meeting? The only thing distinguishing "not arrived" from * "left", since both look like an empty room. */ everRemote: boolean; } export const roomPresence = (f: RoomFacts): RoomPresence | undefined => { // Before `active` there is nobody to be waiting for - the meeting is still being provisioned and // the camera is still coming up. Saying "waiting for the patient" then is noise at best. if (f.phase !== 'active') return undefined; if (f.remoteAttendees.length > 0 || f.remoteTiles.length > 0) return 'occupied'; return f.everRemote ? 'departed' : 'never-arrived'; }; /** IT DEPENDS ON WHO IS READING IT, not only on the kind of meeting - and getting that wrong is how * the PATIENT was told "The patient has left the visit." when their PRACTITIONER left (Spencer, * 2026-08-02, the second browser round). * * That is the SAME defect class as the join banner's "Your provider is waiting" shown to a * clinician, which had been fixed hours earlier - and this copy was written straight afterwards with * the identical mistake. In a two-party call every sentence about "the other person" has two audiences; * a helper that takes only the context can never say the right thing to both. * * NEVER says "the patient" on a `collabteam#` conference: that is the team conferring ABOUT a * patient with the patient deliberately excluded, so naming them there is both wrong and * alarming to whoever reads it. Conferences are practitioner-only, so they need no audience split. */ export const roomPresenceCopy = ( presence: RoomPresence, context: string | undefined, viewerIsClinician: boolean, ): string | undefined => { if (presence === 'occupied') return undefined; const clinical = context?.startsWith('encounter#'); if (!clinical) { return presence === 'never-arrived' ? 'Waiting for others to join…' : 'Everyone else has left the conference.'; } // A 1:1 visit: the OTHER party is the patient if you are the clinician, and the provider if you are // the patient. const other = viewerIsClinician ? 'the patient' : 'your provider'; return presence === 'never-arrived' ? `Waiting for ${other} to join…` : `${viewerIsClinician ? 'The patient' : 'Your provider'} has left the visit.`; };