import type { Communication } from 'fhir/r4'; export interface ThreadSummary { preview: string; timestamp: number; unread: number; /** * Who spoke last -. * * It is the only thing that can answer *"does this owe ME a reply"*, and the feed carried no way to * ask. An unread count goes quiet exactly while the work piles up: a message you READ and did not * answer is still owed, and a read receipt clears the moment somebody merely opens the thread. * `undefined` means nobody has spoken or the last speaker is unattributable - never "it was me". */ lastSenderReference?: string; } const sentMs = (c: Communication): number => (c.sent ? new Date(c.sent).getTime() : 0); const textOf = (c: Communication): string => c.payload?.find((p) => p.contentString)?.contentString ?? ''; /** * YOUR OWN NAME IS NOT HOW YOU REFER TO YOURSELF, and an inbox that says it back is reporting a * conversation you are not in. * * The row's TITLE is the correspondent and is right; this is the line under it, which prefixes whoever * spoke last. When that was the reader it printed their own full name - "Patient One: ..." on Patient * One's own screen, on two of five rows. * * `You` is not a new word for this product: the thread these rows open ALREADY labels the same messages * that way. So the two panes disagreed about the name of one sender, and this is the pane that was wrong. * * `selfRef` is optional because three surfaces call this and only some of them know who is reading; * without it the behaviour is exactly what it was. */ // On a pool thread the patient writes to the office, so whoever answered stays unnamed, as in the thread. // `unnamedAuthor` is a reference the row itself does not name (withheld, unreadable or still loading): a message // it sent is previewed without the stored display, so the line under the row cannot name whom the row withholds. // Matched by reference only; every other author keeps their name. export interface PreviewOptions { othersUnnamed?: boolean; unnamedAuthor?: string } const previewOf = (last: Communication | undefined, selfRef?: string | null, options?: PreviewOptions): string => { if (!last) return ''; const author = last.sender?.reference; const senderName = selfRef && author === selfRef ? 'You' : options?.othersUnnamed || (options?.unnamedAuthor && author === options.unnamedAuthor) ? undefined : last.sender?.display?.trim(); return `${senderName ? `${senderName}: ` : ''}${textOf(last)}`; }; /** * WHAT THIS THREAD SAYS, in one line - for a row that has its own unread count and its own timestamp. * * EXPORTED RATHER THAN RE-DERIVED, and that is the whole point. An inbox row answers *"which * conversation"*; a preview answers *"which conversation needs me FIRST"* - and the moment a second * surface spells out its own version of "the last thing said", the two drift a sentence at a time. That is * the shape of the defect where one screen called a draft 26 days old and another 27. * * `summarizeThread` below and every inbox row now read the same function. */ export const threadPreview = (messages: Communication[], selfRef?: string | null, options?: PreviewOptions): string => previewOf( messages.length === 0 ? undefined : [...messages].sort((a, b) => sentMs(a) - sentMs(b)).at(-1), selfRef, options, ); /** * WHEN THIS THREAD LAST MOVED. The companion to `threadPreview`, and it exists for the same reason: * the two halves of one row have to come from one derivation. * * THE DEFECT. Three conversation lists took their preview from the last MESSAGE and their timestamp * from `CommunicationRequest.authoredOn` - when the CONVERSATION was opened. Two sources for one row, so * the text updated when somebody replied and the time never did. * * AND IT READ AS A TIME IN THE FUTURE, which is the part that makes it look like a clock bug when it * is not. The time was rendered with `toLocaleTimeString`, hour and minute only - so a thread opened at * 5:16 PM on some earlier day displayed "5:16 PM" against a 5:03 PM clock. Nothing was ahead of anything; * a three-day-old timestamp was simply being shown without its date. * * Returns 0 when nothing has been said, which `formatRelativeTime` renders as empty rather than as the * epoch. A row with no messages should say nothing about when, not 1 Jan 1970. */ export const threadLastActivity = (messages: Communication[]): number => { if (messages.length === 0) return 0; return sentMs([...messages].sort((a, b) => sentMs(a) - sentMs(b))[messages.length - 1]); }; export const summarizeThread = ( messages: Communication[], selfRef: string | null, hasReadReceipt: (message: Communication, ref: string) => boolean, options?: PreviewOptions, ): ThreadSummary => { if (messages.length === 0) return { preview: '', timestamp: 0, unread: 0 }; const sorted = [...messages].sort((a, b) => sentMs(a) - sentMs(b)); const last = sorted[sorted.length - 1]; const preview = previewOf(last, selfRef, options); const unread = !selfRef ? 0 : messages.filter((m) => m.sender?.reference !== selfRef && !hasReadReceipt(m, selfRef)).length; return { preview, timestamp: sentMs(last), unread, lastSenderReference: last.sender?.reference }; };