import type { Bundle, Communication } from 'fhir/r4'; import { buildCollabTeamThreadId, buildCollabTeamAsideThreadId, isCollabTeamThreadId, extractTeamIdFromThreadId, isCollabTeamAsideThreadId, extractTeamIdFromAsideThreadId, } from './collaborationTeamThread'; import { summarizeThread, type ThreadSummary } from './threadSummary'; export interface CollabTeamInput { teamId: string; patientId: string; name: string; closed: boolean; } export interface CollabTeamSummary { teamId: string; patientId: string; teamName: string; closed: boolean; public: ThreadSummary; aside: ThreadSummary; /** A thread read failed: no messages here is not "nobody wrote". */ unreadable: boolean; } export interface CollabSummaryDeps { fetchByThread: (threadId: string) => Promise>; selfRef: string | null; /** Whom this viewer's receipts name: their link when acting for someone. Null while unknown, so nothing reads as read. */ receiptRef: string | null; hasReadReceipt: (message: Communication, ref: string) => boolean; } const messagesOf = (bundle: Bundle): Communication[] => (bundle.entry || []).map((e) => e.resource).filter((r): r is Communication => !!r); export const fetchCollabTeamSummaries = async ( teams: CollabTeamInput[], { fetchByThread, selfRef, receiptRef, hasReadReceipt }: CollabSummaryDeps, ): Promise => { // Sender and preview stay the viewer's own account; only whether they read it is asked of the receipt reader. const readBy = (message: Communication) => receiptRef !== null && hasReadReceipt(message, receiptRef); return Promise.all(teams.map(async (team) => { let unreadable = false; const load = (threadId: string) => fetchByThread(threadId).then(messagesOf).catch(() => { unreadable = true; return [] as Communication[]; }); const [pub, aside] = await Promise.all([ load(buildCollabTeamThreadId(team.teamId)), load(buildCollabTeamAsideThreadId(team.teamId)), ]); return { teamId: team.teamId, patientId: team.patientId, teamName: team.name, closed: team.closed, public: summarizeThread(pub, selfRef, readBy), aside: summarizeThread(aside, selfRef, readBy), unreadable, }; })); }; // `unreadable`: a thread read failed, so no preview and no unread count is not "nobody wrote". export interface TeamMessageSummary { unread: number; preview: string; timestamp: number; unreadable?: boolean; } // Merge a team's public + (optional) aside ThreadSummary into one badge summary: unread is the sum; // preview/timestamp come from whichever channel has the more recent message. export const combineTeamSummary = (pub: ThreadSummary, aside: ThreadSummary | null): TeamMessageSummary => { const unread = pub.unread + (aside?.unread ?? 0); const most = aside && aside.timestamp > pub.timestamp ? aside : pub; return { unread, preview: most.preview, timestamp: most.timestamp }; }; // Map an incoming WS event's thread-id to (teamId, variant) IFF it belongs to a visible team. // Aside ids resolve only when includeAside - the patient path must never react to aside traffic. export const resolveCollabEventThread = ( threadId: string, teamIds: Set, includeAside: boolean, ): { teamId: string; variant: 'public' | 'aside' } | null => { if (isCollabTeamThreadId(threadId)) { const teamId = extractTeamIdFromThreadId(threadId); return teamIds.has(teamId) ? { teamId, variant: 'public' } : null; } if (includeAside && isCollabTeamAsideThreadId(threadId)) { const teamId = extractTeamIdFromAsideThreadId(threadId); return teamIds.has(teamId) ? { teamId, variant: 'aside' } : null; } return null; };