/** * sub-task 5b - the meeting composer's UNSENT text, held per thread, outside the component. * * WHY. `MeetingMessageComposer` kept its text in local `useState`, which was safe only while the * composer was permanently mounted: before the collapse ladder, every granted panel rendered side by side * for the whole call. At the `tabs` and `icons` rungs only the ACTIVE panel body mounts, so switching to * Notes and back - or resizing across a rung boundary, or minimising and restoring the window - unmounts * the composer and takes whatever the clinician was mid-typing with it. A practitioner typing * "BP was 150/95, will recheck" in a 700px window lost it by clicking one tab. * * This is the same defect class was raised for, and it is fixed the same way this feature already * fixed its two siblings: the clinical-note draft lives in the page-level notes store * (`useEncounterClinicalNote` `EncounterNotesContext`) and the public/private tab selection lives in * `encounterDocumentationTabMemory.ts` - both OUTSIDE the component that can be unmounted by a layout * change. The chat composer was the one that still had no such store; this is it, shaped after the tab * memory it sits beside. * * Keyed per THREAD, deliberately - the same reason the tab memory is keyed per encounter. A case * conference points ONE mounted composer at two different threads (`collabteam#` and the practitioner-only * `collabaside#`) as the aside toggle flips, and a draft written for the team must never surface in the * aside composer, where sending it would post it to a different audience. * * IN MEMORY ONLY. Never `localStorage`, never `sessionStorage` - message text is PHI, and this window's * geometry key (`akashic_floating_window_meeting-window`) already survives logout on a shared clinic * workstation. Nothing clinical goes anywhere near browser storage. Same rule the note drafts carry. */ const drafts = new Map(); // Bumped on every write, so an answer that arrives later can tell whether the draft it was sent from is still the one held. const revisions = new Map(); /** How many times `threadId`'s draft has been written; a send captures it and clears only an unchanged draft. */ export function composerDraftRevision(threadId: string | null | undefined): number { return threadId ? revisions.get(threadId) ?? 0 : 0; } /** The unsent text for `threadId`, or `''`. A `null` id (a context whose thread cannot be named) has no * draft by construction - such a thread cannot be sent to either. */ export function readComposerDraft(threadId: string | null | undefined): string { return threadId ? drafts.get(threadId) ?? '' : ''; } /** Records what is currently typed. An EMPTY composer deletes its entry rather than storing `''` - there is * nothing to remember, and the less PHI-shaped text sitting in this map the better. */ export function writeComposerDraft(threadId: string | null | undefined, text: string): void { if (!threadId) return; // nothing to key on - a draft with no thread is not worth remembering revisions.set(threadId, composerDraftRevision(threadId) + 1); if (!text) { drafts.delete(threadId); return; } drafts.set(threadId, text); } /** Forgets a thread's draft - on send (it is in the record now) and when the call it belonged to ends. */ export function clearComposerDraft(threadId: string | null | undefined): void { if (!threadId) return; drafts.delete(threadId); }