// The single app-level video mount. Rendered as a CHILD of #akashic-overlay-root (see App.tsx) rather
// than portalling into it: the provider mounts far above where that div is rendered, so a portal would
// resolve a null container on first render. Being a child is equivalent for CSS containment and
// removes the ordering hazard entirely.
//
// `MeetingWindow` DOES portal into that same div, through `FloatingWindow`. That is safe
// for the reason above rather than in spite of it: a call cannot be live on the first render - the
// phase starts `idle` and this component renders nothing at all until a user action moves it - so by
// the time the window mounts, #akashic-overlay-root has long been in the document. What it does NOT
// survive is that div's subtree REMOUNTING mid-call: `getAkashicPortalContainer()` would resolve null
// for that pass and `FloatingWindow` fails closed (renders nothing) with no retry. Nothing remounts it
// today; a future conditional above it would need this checked.
//
// Task 13 added the join affordance ("a meeting exists for you"). It renders ALONGSIDE the live
// surface rather than instead of it: `call.available` is separate state from `call.phase`, so being
// offered a meeting while already in one is a legitimate, simultaneous fact.
import { useEffect, useState } from 'react';
import { AlertTriangle, X } from 'lucide-react';
import { useVideoServiceContext } from '../../services/VideoService';
import { AvailableMeetingBanner } from './AvailableMeetingBanner';
import { MeetingWindow } from './MeetingWindow';
/** What is left to say once the call itself is gone. Deliberately NOT the call chrome: there is
* nothing to mute, no camera to turn off and nobody to leave - only something the user needs to
* know. It carries no `VideoTileGrid`, so it cannot make tile binding ambiguous. */
const CallFailureNotice = ({ message, onDismiss }: { message: string; onDismiss: () => void }) => (
);
/** Deliberately NOT `CallFailureNotice`. Being removed from a visit is not a failure and the person
* reading it may be a patient in crisis, so it carries no alert colour and no warning icon - but it
* is a `role="status"` announcement, because a screen reader user is otherwise dropped in silence. */
/** Renamed from `CallRemovedNotice` 2026-09-14: it carries two notices now - a removal, and a visit
* somebody else ended - and a name that says `removed` on the second one is a comment that lies. */
const CallNotice = ({ message, onDismiss }: { message: string; onDismiss: () => void }) => (
);
/** TWO SURFACES AND AN ESCAPE HATCH ONCE LIVED HERE, and deleted all three:
*
* 1. `DockedCallEscapeHatch` - a Leave-only stub with no grid, no mute and no camera, shown when a
* "docked" call had no docked surface mounted. Task 13 replaced it with the full floating surface.
* 2. `FloatingCallSurface` - the 22rem pane parked bottom-right.
* 3. The ARBITRATION between the floating and docked surfaces (`dockedSurfaceMounted`), whose only
* job was keeping exactly one `VideoTileGrid` mounted.
*
* `MeetingWindow` is now the single in-call surface, so the one-grid rule is structural and there is
* no surface decision left to take. See the header of `MeetingWindow.tsx` for the two defects the
* two-surface shape produced - both of them were surface-CHOICE defects. */
export const CallOverlay = () => {
const { call } = useVideoServiceContext();
// NOT `phase === 'active'`. Every non-idle phase is a call the user is in the middle of:
// `creating`/`joining` is someone who clicked Start and is owed feedback while the meeting is
// provisioned and the camera comes up, and `ending` is the only thing that keeps the disabled
// "Leaving..." state on screen - the one moment a second click on Leave must be impossible.
// Anchored on `idle` rather than listing the live phases so that a phase added later fails SAFE:
// it shows the call, instead of silently vanishing it.
const live = call.phase !== 'idle';
const [dismissed, setDismissed] = useState(false);
// A NEW failure must never be hidden by the dismissal of the previous one. Every route to a new
// failure passes through `error: undefined` first (`START` and `JOIN_AVAILABLE` both clear it),
// so resetting when the error clears is exact rather than approximate.
useEffect(() => { if (!call.error) setDismissed(false); }, [call.error]);
// Tracked separately from the failure dismissal: a notice and a call failure are different
// messages and dismissing one must not silence the other.
const [noticeDismissed, setNoticeDismissed] = useState(false);
useEffect(() => { if (!call.notice) setNoticeDismissed(false); }, [call.notice]);
// THERE IS NO SURFACE DECISION ANY MORE, and that is the point of. A live call gets
// the meeting window; every viewer, every context. The two defects this feature shipped were both
// decisions ABOUT the viewer taken from a fact about the MEETING (`presentation`, then
// `dockedSurfaceMounted`) - a question that cannot be answered wrongly if it is never asked.
const surface = !live
// A CALL-LIFECYCLE FAILURE OUTLIVES THE CALL, and that is the whole reason this branch
// exists. A `/leave` that reached the broker and failed leaves this user very possibly still
// a participant in a live, BILLING meeting - while the provider's teardown has already run
// and returned the phase to idle. Every in-call surface is unmounted by then, so a failure
// held in the chrome's own state (`CallControls` used to do exactly that) can never reach the
// screen. This overlay is the piece that survives, so this is where it gets reported. Same
// for a refused `startCall`/`joinAvailable`, which had no reporter at all before this.
? (!call.error || dismissed
? null
: setDismissed(true)} />)
: ;
// The banner is OUTSIDE that decision on purpose: `available` is separate state from `phase`, so
// a meeting you may join can legitimately be on screen next to the call you are already in.
// Outside the surface decision for the same reason the banner is, and for one of its own: a removal
// notice has to survive the teardown that returns `phase` to idle, which unmounts every in-call
// surface that could otherwise have carried it.
return (
<>
{surface}
{call.notice && !noticeDismissed && (
setNoticeDismissed(true)} />
)}
>
);
};