@natoe/colab 0.1.28 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -119,25 +119,40 @@ interface PatientData {
119
119
  bodyParts?: string[];
120
120
  }
121
121
  /**
122
- * Which thread on a case this is.
122
+ * What a thread is about and, crucially, what it is SCOPED to.
123
123
  *
124
- * - `case` — the shared discussion: lab, radiologist, physician, admin
125
- * - `help_lab` imaging center <-> Natoe support, private to that side
126
- * - `help_radiologist` — assigned radiologist <-> Natoe support, private to that side
124
+ * - `case` — the discussion about one clinical order. Scoped to an order,
125
+ * so `orderId` is always set. Lab, radiologist, physician, admin.
126
+ * - `help_lab` — an imaging centre's help channel with Natoe support. Scoped to
127
+ * the LAB, not to any case: it has NO `orderId`, it outlives
128
+ * every order, and there is exactly one per imaging centre.
127
129
  *
128
- * The two help kinds are mutually invisible. That isolation is enforced by the
129
- * backend's participant seeding, never by anything in this package treat this
130
- * as a label for rendering and for addressing the right thread, not as a
131
- * security boundary.
130
+ * `help_lab` was briefly modelled as per-case (one help thread per order). It is
131
+ * now per-lab, so support gets one continuous thread with each imaging centre
132
+ * rather than N short-lived ones scattered across cases. A case can still be
133
+ * referenced from inside the channel; it just no longer scopes it.
134
+ *
135
+ * Isolation — who can see a help channel — is enforced by the backend's
136
+ * participant seeding, never by anything in this package. Treat this as a label
137
+ * for rendering and for addressing the right thread, not as a security boundary.
132
138
  *
133
139
  * Optional everywhere, defaulting to `case`, so callers written before help
134
- * threads existed keep their exact previous behaviour.
140
+ * channels existed keep their exact previous behaviour.
135
141
  */
136
- type ConversationKind = 'case' | 'help_lab' | 'help_radiologist';
142
+ type ConversationKind = 'case' | 'help_lab';
137
143
  declare function isHelpKind(kind?: ConversationKind | string): boolean;
144
+ /**
145
+ * True when a thread is scoped to an order and therefore has patient context to
146
+ * render. Prefer this over testing `orderId` directly: a help channel has no
147
+ * order, so every patient-shaped affordance must be skipped for it.
148
+ */
149
+ declare function isCaseKind(kind?: ConversationKind | string): boolean;
138
150
  interface Conversation {
139
151
  id: string;
140
- orderId: string;
152
+ /** Absent on a help channel — that thread belongs to a lab, not an order. */
153
+ orderId?: string;
154
+ /** Set only on a help channel: the imaging centre that owns it. */
155
+ labUserId?: string;
141
156
  name: string;
142
157
  kind?: ConversationKind;
143
158
  picture?: string;
@@ -146,7 +161,8 @@ interface Conversation {
146
161
  unreadCount: number;
147
162
  createdAt: string;
148
163
  updatedAt: string;
149
- /** Denormalized patient data snapshot — lets UI render without extra fetch */
164
+ /** Denormalized patient data snapshot — lets UI render without extra fetch.
165
+ * Always absent on a help channel: there is no patient. */
150
166
  patientSnapshot?: PatientData;
151
167
  /** Currently pinned messages (max 3) */
152
168
  pinnedMessages?: Message[];
@@ -157,10 +173,12 @@ interface Conversation {
157
173
  */
158
174
  interface ConversationListItem$1 {
159
175
  id: string;
160
- orderId: string;
176
+ /** Absent on a help channel — see `Conversation.orderId`. */
177
+ orderId?: string;
178
+ labUserId?: string;
161
179
  name: string;
162
- /** Lets the inbox label help threads distinctly from the case thread on the
163
- * same patient both appear as separate rows. */
180
+ /** Lets the inbox render the lab's help channel distinctly from its case
181
+ * threadsthey appear as separate rows. */
164
182
  kind?: ConversationKind;
165
183
  picture?: string;
166
184
  unreadCount: number;
@@ -285,10 +303,15 @@ interface InviteUserPayload {
285
303
  }
286
304
 
287
305
  interface CollabPanelProps {
288
- /** Order ID one conversation per study */
289
- orderId: string;
290
- /** Patient/study data displayed in the header */
291
- patientData: PatientData;
306
+ /** Order this thread belongs to. Required for a `case` thread; omitted for a
307
+ * help channel, which belongs to a lab rather than an order. */
308
+ orderId?: string;
309
+ /** A thread the caller already resolved — how a help channel is opened.
310
+ * Skips the order-keyed preview lookup entirely. */
311
+ conversation?: Conversation | null;
312
+ /** Patient/study data displayed in the header. Absent on a help channel:
313
+ * there is no patient, and the header renders its support variant instead. */
314
+ patientData?: PatientData;
292
315
  /** Participant user IDs — only used when creating a brand-new conversation */
293
316
  participantIds?: string[];
294
317
  /** Which thread on the case. Defaults to the shared `case` thread. */
@@ -343,13 +366,16 @@ interface CollabPanelProps {
343
366
  * Full collaboration panel: patient header + pinned bar + message thread + input.
344
367
  * Supports reply, pin/unpin, and (optionally) seen-by indicators.
345
368
  */
346
- declare function CollabPanel({ orderId, patientData, participantIds, kind, showSeenBy, onBack, hidePatientName, hideOpenCase, onConversationChange, themeMode, showSettings: controlledShowSettings, onSettingsChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
369
+ declare function CollabPanel({ orderId, conversation: providedConversation, patientData, participantIds, kind, showSeenBy, onBack, hidePatientName, hideOpenCase, onConversationChange, themeMode, showSettings: controlledShowSettings, onSettingsChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
347
370
 
348
371
  interface CollabPopupProps {
349
- /** Order ID for this conversation */
350
- orderId: string;
372
+ /** Order this thread belongs to. Required for a `case` thread; omitted for a
373
+ * help channel, which belongs to a lab rather than an order. */
374
+ orderId?: string;
375
+ /** A thread the caller already resolved — how a help channel is opened. */
376
+ conversation?: Conversation | null;
351
377
  /** Patient/study data */
352
- patientData: PatientData;
378
+ patientData?: PatientData;
353
379
  /** Participant user IDs — only used when creating a brand-new conversation */
354
380
  participantIds?: string[];
355
381
  /** Which thread on the case. Defaults to the shared `case` thread. */
@@ -383,7 +409,7 @@ interface CollabPopupProps {
383
409
  * Draggable floating chat popup — drop-in replacement for DraggableChatPopup.
384
410
  * Renders a CollabPanel inside a movable, resizable container.
385
411
  */
386
- declare function CollabPopup({ orderId, patientData, participantIds, kind, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
412
+ declare function CollabPopup({ orderId, conversation: providedConversation, patientData, participantIds, kind, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
387
413
 
388
414
  type ColabInlineMode = 'light' | 'dark';
389
415
  interface CollabInlineProps {
@@ -635,19 +661,36 @@ interface CollabProviderProps {
635
661
  declare function CollabProvider({ config, apiBaseUrl, children }: CollabProviderProps): react_jsx_runtime.JSX.Element | null;
636
662
 
637
663
  interface UseConversationOptions {
638
- orderId: string;
639
- patientData: PatientData;
664
+ /**
665
+ * The order this thread belongs to. Required for a `case` thread, which is
666
+ * created lazily on first message and resolved by an order-keyed preview.
667
+ * Omitted for a help channel — that belongs to a lab, not an order.
668
+ */
669
+ orderId?: string;
670
+ /**
671
+ * A thread the caller has ALREADY resolved, opened directly instead of being
672
+ * looked up by order.
673
+ *
674
+ * This is how a help channel opens. The backend's `GET /help_channel` does
675
+ * the get-or-create and hands back the conversation, so there is nothing to
676
+ * look up and nothing to lazily create — passing it here skips the preview
677
+ * request entirely. The preview endpoint is order-keyed and could not serve
678
+ * an order-less thread in any case.
679
+ */
680
+ conversation?: Conversation | null;
681
+ /** Patient/study context. Absent on a help channel — there is no patient. */
682
+ patientData?: PatientData;
640
683
  /** Participant user IDs used only when creating a brand-new conversation */
641
684
  participantIds?: string[];
642
685
  /** Load full history on mount (default true) */
643
686
  loadHistory?: boolean;
644
687
  /**
645
- * Which thread on the case to open. Defaults to the shared `case` thread,
646
- * so every existing caller is unaffected.
688
+ * What this thread is. Defaults to the shared `case` thread, so every
689
+ * existing caller is unaffected.
647
690
  *
648
- * For a help kind the backend seats only the requesting side plus Natoe
649
- * support, and `participantIds` is ignored server-side — a help thread
650
- * must never be widened by the client.
691
+ * For `help_lab` the backend seats only the imaging centre plus Natoe
692
+ * support, and `participantIds` is ignored server-side — a help channel must
693
+ * never be widened by the client.
651
694
  */
652
695
  kind?: ConversationKind;
653
696
  }
@@ -684,7 +727,7 @@ interface UseConversationReturn {
684
727
  * when the user sends the first message. If a conversation already exists for
685
728
  * the given orderId, it's loaded and joined automatically.
686
729
  */
687
- declare function useConversation({ orderId, patientData, participantIds, loadHistory, kind, }: UseConversationOptions): UseConversationReturn;
730
+ declare function useConversation({ orderId, conversation: providedConversation, patientData, participantIds, loadHistory, kind, }: UseConversationOptions): UseConversationReturn;
688
731
 
689
732
  interface UseConversationListOptions {
690
733
  /**
@@ -950,6 +993,13 @@ interface PatientHeaderProps {
950
993
  hideName?: boolean;
951
994
  /** Override the displayed name — use the stored conversation.name to avoid reconstructing from patientData components */
952
995
  displayName?: string;
996
+ /**
997
+ * Render the support variant: a help channel has no patient, no study and no
998
+ * case, so the demographics line and every case-level action are suppressed
999
+ * and the title is the channel name. Without this the header would show a
1000
+ * row of empty fields and a dead "View DICOM" button.
1001
+ */
1002
+ isSupportChannel?: boolean;
953
1003
  className?: string;
954
1004
  }
955
1005
  /**
@@ -967,7 +1017,7 @@ interface PatientHeaderProps {
967
1017
  * When `hideName=true` (CollabPopup wraps the conversation in its own
968
1018
  * window title), only the meta + actions row renders.
969
1019
  */
970
- declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenCase, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
1020
+ declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenCase, onOpenSettings, onBack, hideName, displayName, isSupportChannel, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
971
1021
 
972
1022
  interface MessageListHandle {
973
1023
  /** Scroll to a specific message by ID (used by pin jump-to) */
@@ -1230,4 +1280,4 @@ declare const SUPPORTED_IMAGE_TYPES: string[];
1230
1280
  /** Max file size in bytes (20MB) */
1231
1281
  declare const MAX_FILE_SIZE: number;
1232
1282
 
1233
- export { AUDIO_MIME_TYPE, type ChannelEvent, ChannelSettings, type ChannelUpdatePayload, type CollabConfig, type CollabError, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, type Conversation, type ConversationKind, ConversationList, ConversationListItem, type ConversationListItem$1 as ConversationListItemData, type ConversationPreview, type CreateConversationResponse, DEEP_LINK_PREFIX, EVENTS, type InviteUserPayload, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, type Message, MessageActionsMenu, MessageBubble, MessageInput, MessageList, type MessageListHandle, type MessageSnapshot, type MessageType, type Participant, ParticipantsList, type PatientData, PatientHeader, PinnedMessagesBar, type PreviewBatchResponse, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, type SendMessagePayload, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, type Theme, type TypingEvent, type UserRole, applyThemeOverrides, isHelpKind, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
1283
+ export { AUDIO_MIME_TYPE, type ChannelEvent, ChannelSettings, type ChannelUpdatePayload, type CollabConfig, type CollabError, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, type Conversation, type ConversationKind, ConversationList, ConversationListItem, type ConversationListItem$1 as ConversationListItemData, type ConversationPreview, type CreateConversationResponse, DEEP_LINK_PREFIX, EVENTS, type InviteUserPayload, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, type Message, MessageActionsMenu, MessageBubble, MessageInput, MessageList, type MessageListHandle, type MessageSnapshot, type MessageType, type Participant, ParticipantsList, type PatientData, PatientHeader, PinnedMessagesBar, type PreviewBatchResponse, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, type SendMessagePayload, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, type Theme, type TypingEvent, type UserRole, applyThemeOverrides, isCaseKind, isHelpKind, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
package/dist/index.d.ts CHANGED
@@ -119,25 +119,40 @@ interface PatientData {
119
119
  bodyParts?: string[];
120
120
  }
121
121
  /**
122
- * Which thread on a case this is.
122
+ * What a thread is about and, crucially, what it is SCOPED to.
123
123
  *
124
- * - `case` — the shared discussion: lab, radiologist, physician, admin
125
- * - `help_lab` imaging center <-> Natoe support, private to that side
126
- * - `help_radiologist` — assigned radiologist <-> Natoe support, private to that side
124
+ * - `case` — the discussion about one clinical order. Scoped to an order,
125
+ * so `orderId` is always set. Lab, radiologist, physician, admin.
126
+ * - `help_lab` — an imaging centre's help channel with Natoe support. Scoped to
127
+ * the LAB, not to any case: it has NO `orderId`, it outlives
128
+ * every order, and there is exactly one per imaging centre.
127
129
  *
128
- * The two help kinds are mutually invisible. That isolation is enforced by the
129
- * backend's participant seeding, never by anything in this package treat this
130
- * as a label for rendering and for addressing the right thread, not as a
131
- * security boundary.
130
+ * `help_lab` was briefly modelled as per-case (one help thread per order). It is
131
+ * now per-lab, so support gets one continuous thread with each imaging centre
132
+ * rather than N short-lived ones scattered across cases. A case can still be
133
+ * referenced from inside the channel; it just no longer scopes it.
134
+ *
135
+ * Isolation — who can see a help channel — is enforced by the backend's
136
+ * participant seeding, never by anything in this package. Treat this as a label
137
+ * for rendering and for addressing the right thread, not as a security boundary.
132
138
  *
133
139
  * Optional everywhere, defaulting to `case`, so callers written before help
134
- * threads existed keep their exact previous behaviour.
140
+ * channels existed keep their exact previous behaviour.
135
141
  */
136
- type ConversationKind = 'case' | 'help_lab' | 'help_radiologist';
142
+ type ConversationKind = 'case' | 'help_lab';
137
143
  declare function isHelpKind(kind?: ConversationKind | string): boolean;
144
+ /**
145
+ * True when a thread is scoped to an order and therefore has patient context to
146
+ * render. Prefer this over testing `orderId` directly: a help channel has no
147
+ * order, so every patient-shaped affordance must be skipped for it.
148
+ */
149
+ declare function isCaseKind(kind?: ConversationKind | string): boolean;
138
150
  interface Conversation {
139
151
  id: string;
140
- orderId: string;
152
+ /** Absent on a help channel — that thread belongs to a lab, not an order. */
153
+ orderId?: string;
154
+ /** Set only on a help channel: the imaging centre that owns it. */
155
+ labUserId?: string;
141
156
  name: string;
142
157
  kind?: ConversationKind;
143
158
  picture?: string;
@@ -146,7 +161,8 @@ interface Conversation {
146
161
  unreadCount: number;
147
162
  createdAt: string;
148
163
  updatedAt: string;
149
- /** Denormalized patient data snapshot — lets UI render without extra fetch */
164
+ /** Denormalized patient data snapshot — lets UI render without extra fetch.
165
+ * Always absent on a help channel: there is no patient. */
150
166
  patientSnapshot?: PatientData;
151
167
  /** Currently pinned messages (max 3) */
152
168
  pinnedMessages?: Message[];
@@ -157,10 +173,12 @@ interface Conversation {
157
173
  */
158
174
  interface ConversationListItem$1 {
159
175
  id: string;
160
- orderId: string;
176
+ /** Absent on a help channel — see `Conversation.orderId`. */
177
+ orderId?: string;
178
+ labUserId?: string;
161
179
  name: string;
162
- /** Lets the inbox label help threads distinctly from the case thread on the
163
- * same patient both appear as separate rows. */
180
+ /** Lets the inbox render the lab's help channel distinctly from its case
181
+ * threadsthey appear as separate rows. */
164
182
  kind?: ConversationKind;
165
183
  picture?: string;
166
184
  unreadCount: number;
@@ -285,10 +303,15 @@ interface InviteUserPayload {
285
303
  }
286
304
 
287
305
  interface CollabPanelProps {
288
- /** Order ID one conversation per study */
289
- orderId: string;
290
- /** Patient/study data displayed in the header */
291
- patientData: PatientData;
306
+ /** Order this thread belongs to. Required for a `case` thread; omitted for a
307
+ * help channel, which belongs to a lab rather than an order. */
308
+ orderId?: string;
309
+ /** A thread the caller already resolved — how a help channel is opened.
310
+ * Skips the order-keyed preview lookup entirely. */
311
+ conversation?: Conversation | null;
312
+ /** Patient/study data displayed in the header. Absent on a help channel:
313
+ * there is no patient, and the header renders its support variant instead. */
314
+ patientData?: PatientData;
292
315
  /** Participant user IDs — only used when creating a brand-new conversation */
293
316
  participantIds?: string[];
294
317
  /** Which thread on the case. Defaults to the shared `case` thread. */
@@ -343,13 +366,16 @@ interface CollabPanelProps {
343
366
  * Full collaboration panel: patient header + pinned bar + message thread + input.
344
367
  * Supports reply, pin/unpin, and (optionally) seen-by indicators.
345
368
  */
346
- declare function CollabPanel({ orderId, patientData, participantIds, kind, showSeenBy, onBack, hidePatientName, hideOpenCase, onConversationChange, themeMode, showSettings: controlledShowSettings, onSettingsChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
369
+ declare function CollabPanel({ orderId, conversation: providedConversation, patientData, participantIds, kind, showSeenBy, onBack, hidePatientName, hideOpenCase, onConversationChange, themeMode, showSettings: controlledShowSettings, onSettingsChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
347
370
 
348
371
  interface CollabPopupProps {
349
- /** Order ID for this conversation */
350
- orderId: string;
372
+ /** Order this thread belongs to. Required for a `case` thread; omitted for a
373
+ * help channel, which belongs to a lab rather than an order. */
374
+ orderId?: string;
375
+ /** A thread the caller already resolved — how a help channel is opened. */
376
+ conversation?: Conversation | null;
351
377
  /** Patient/study data */
352
- patientData: PatientData;
378
+ patientData?: PatientData;
353
379
  /** Participant user IDs — only used when creating a brand-new conversation */
354
380
  participantIds?: string[];
355
381
  /** Which thread on the case. Defaults to the shared `case` thread. */
@@ -383,7 +409,7 @@ interface CollabPopupProps {
383
409
  * Draggable floating chat popup — drop-in replacement for DraggableChatPopup.
384
410
  * Renders a CollabPanel inside a movable, resizable container.
385
411
  */
386
- declare function CollabPopup({ orderId, patientData, participantIds, kind, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
412
+ declare function CollabPopup({ orderId, conversation: providedConversation, patientData, participantIds, kind, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
387
413
 
388
414
  type ColabInlineMode = 'light' | 'dark';
389
415
  interface CollabInlineProps {
@@ -635,19 +661,36 @@ interface CollabProviderProps {
635
661
  declare function CollabProvider({ config, apiBaseUrl, children }: CollabProviderProps): react_jsx_runtime.JSX.Element | null;
636
662
 
637
663
  interface UseConversationOptions {
638
- orderId: string;
639
- patientData: PatientData;
664
+ /**
665
+ * The order this thread belongs to. Required for a `case` thread, which is
666
+ * created lazily on first message and resolved by an order-keyed preview.
667
+ * Omitted for a help channel — that belongs to a lab, not an order.
668
+ */
669
+ orderId?: string;
670
+ /**
671
+ * A thread the caller has ALREADY resolved, opened directly instead of being
672
+ * looked up by order.
673
+ *
674
+ * This is how a help channel opens. The backend's `GET /help_channel` does
675
+ * the get-or-create and hands back the conversation, so there is nothing to
676
+ * look up and nothing to lazily create — passing it here skips the preview
677
+ * request entirely. The preview endpoint is order-keyed and could not serve
678
+ * an order-less thread in any case.
679
+ */
680
+ conversation?: Conversation | null;
681
+ /** Patient/study context. Absent on a help channel — there is no patient. */
682
+ patientData?: PatientData;
640
683
  /** Participant user IDs used only when creating a brand-new conversation */
641
684
  participantIds?: string[];
642
685
  /** Load full history on mount (default true) */
643
686
  loadHistory?: boolean;
644
687
  /**
645
- * Which thread on the case to open. Defaults to the shared `case` thread,
646
- * so every existing caller is unaffected.
688
+ * What this thread is. Defaults to the shared `case` thread, so every
689
+ * existing caller is unaffected.
647
690
  *
648
- * For a help kind the backend seats only the requesting side plus Natoe
649
- * support, and `participantIds` is ignored server-side — a help thread
650
- * must never be widened by the client.
691
+ * For `help_lab` the backend seats only the imaging centre plus Natoe
692
+ * support, and `participantIds` is ignored server-side — a help channel must
693
+ * never be widened by the client.
651
694
  */
652
695
  kind?: ConversationKind;
653
696
  }
@@ -684,7 +727,7 @@ interface UseConversationReturn {
684
727
  * when the user sends the first message. If a conversation already exists for
685
728
  * the given orderId, it's loaded and joined automatically.
686
729
  */
687
- declare function useConversation({ orderId, patientData, participantIds, loadHistory, kind, }: UseConversationOptions): UseConversationReturn;
730
+ declare function useConversation({ orderId, conversation: providedConversation, patientData, participantIds, loadHistory, kind, }: UseConversationOptions): UseConversationReturn;
688
731
 
689
732
  interface UseConversationListOptions {
690
733
  /**
@@ -950,6 +993,13 @@ interface PatientHeaderProps {
950
993
  hideName?: boolean;
951
994
  /** Override the displayed name — use the stored conversation.name to avoid reconstructing from patientData components */
952
995
  displayName?: string;
996
+ /**
997
+ * Render the support variant: a help channel has no patient, no study and no
998
+ * case, so the demographics line and every case-level action are suppressed
999
+ * and the title is the channel name. Without this the header would show a
1000
+ * row of empty fields and a dead "View DICOM" button.
1001
+ */
1002
+ isSupportChannel?: boolean;
953
1003
  className?: string;
954
1004
  }
955
1005
  /**
@@ -967,7 +1017,7 @@ interface PatientHeaderProps {
967
1017
  * When `hideName=true` (CollabPopup wraps the conversation in its own
968
1018
  * window title), only the meta + actions row renders.
969
1019
  */
970
- declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenCase, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
1020
+ declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenCase, onOpenSettings, onBack, hideName, displayName, isSupportChannel, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
971
1021
 
972
1022
  interface MessageListHandle {
973
1023
  /** Scroll to a specific message by ID (used by pin jump-to) */
@@ -1230,4 +1280,4 @@ declare const SUPPORTED_IMAGE_TYPES: string[];
1230
1280
  /** Max file size in bytes (20MB) */
1231
1281
  declare const MAX_FILE_SIZE: number;
1232
1282
 
1233
- export { AUDIO_MIME_TYPE, type ChannelEvent, ChannelSettings, type ChannelUpdatePayload, type CollabConfig, type CollabError, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, type Conversation, type ConversationKind, ConversationList, ConversationListItem, type ConversationListItem$1 as ConversationListItemData, type ConversationPreview, type CreateConversationResponse, DEEP_LINK_PREFIX, EVENTS, type InviteUserPayload, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, type Message, MessageActionsMenu, MessageBubble, MessageInput, MessageList, type MessageListHandle, type MessageSnapshot, type MessageType, type Participant, ParticipantsList, type PatientData, PatientHeader, PinnedMessagesBar, type PreviewBatchResponse, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, type SendMessagePayload, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, type Theme, type TypingEvent, type UserRole, applyThemeOverrides, isHelpKind, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
1283
+ export { AUDIO_MIME_TYPE, type ChannelEvent, ChannelSettings, type ChannelUpdatePayload, type CollabConfig, type CollabError, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, type Conversation, type ConversationKind, ConversationList, ConversationListItem, type ConversationListItem$1 as ConversationListItemData, type ConversationPreview, type CreateConversationResponse, DEEP_LINK_PREFIX, EVENTS, type InviteUserPayload, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, type Message, MessageActionsMenu, MessageBubble, MessageInput, MessageList, type MessageListHandle, type MessageSnapshot, type MessageType, type Participant, ParticipantsList, type PatientData, PatientHeader, PinnedMessagesBar, type PreviewBatchResponse, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, type SendMessagePayload, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, type Theme, type TypingEvent, type UserRole, applyThemeOverrides, isCaseKind, isHelpKind, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
package/dist/index.js CHANGED
@@ -891,6 +891,7 @@ function cleanChannelName(name) {
891
891
  // src/hooks/useConversation.ts
892
892
  function useConversation({
893
893
  orderId,
894
+ conversation: providedConversation,
894
895
  patientData,
895
896
  participantIds = [],
896
897
  loadHistory = true,
@@ -923,6 +924,7 @@ function useConversation({
923
924
  const markedReadIdsRef = React4.useRef(/* @__PURE__ */ new Set());
924
925
  const buildName = React4.useCallback(
925
926
  () => {
927
+ if (!patientData) return "";
926
928
  const labName = patientData.labName ?? (config.userRole === "lab" ? config.userName : void 0);
927
929
  return buildChannelName({ ...patientData, labName });
928
930
  },
@@ -1022,6 +1024,28 @@ function useConversation({
1022
1024
  setIsLoading(true);
1023
1025
  setError(null);
1024
1026
  try {
1027
+ if (providedConversation) {
1028
+ setConversation(providedConversation);
1029
+ setParticipants(providedConversation.participants ?? []);
1030
+ joinChannel(providedConversation);
1031
+ if (loadHistory) {
1032
+ const history = await fetchMessages(providedConversation.id);
1033
+ if (cancelled) return;
1034
+ setMessages(history);
1035
+ setHasMore(history.length >= MESSAGES_PAGE_SIZE);
1036
+ setPinnedMessages(history.filter((m) => m.isPinned));
1037
+ }
1038
+ if (!cancelled) setIsLoading(false);
1039
+ return;
1040
+ }
1041
+ if (!orderId) {
1042
+ setConversation(null);
1043
+ setMessages([]);
1044
+ setParticipants([]);
1045
+ setHasMore(false);
1046
+ setIsLoading(false);
1047
+ return;
1048
+ }
1025
1049
  const preview = await requestPreview(orderId, kind);
1026
1050
  if (cancelled) return;
1027
1051
  if (!preview) {
@@ -1083,6 +1107,12 @@ function useConversation({
1083
1107
  const ensureConversation = React4.useCallback(
1084
1108
  async (payload) => {
1085
1109
  if (conversation) return { conv: conversation, persistedByCreate: false };
1110
+ if (!orderId) {
1111
+ throw {
1112
+ code: "NO_CONVERSATION",
1113
+ message: "This conversation is not available yet. Please reopen it and try again."
1114
+ };
1115
+ }
1086
1116
  if (ensureConversationInFlight.current) {
1087
1117
  const conv = await ensureConversationInFlight.current;
1088
1118
  return { conv, persistedByCreate: false };
@@ -1570,15 +1600,19 @@ function PatientHeader({
1570
1600
  onBack,
1571
1601
  hideName = false,
1572
1602
  displayName,
1603
+ isSupportChannel = false,
1573
1604
  className
1574
1605
  }) {
1575
- const hasDicom = !!(patientData.studyId && patientData.storageId);
1576
- const resolvedName = resolveDisplayName(patientData, displayName);
1606
+ const hasDicom = !isSupportChannel && !!(patientData.studyId && patientData.storageId);
1607
+ const resolvedName = isSupportChannel ? displayName || patientData.patientName || "Natoe Support" : resolveDisplayName(patientData, displayName);
1577
1608
  const metaParts = [];
1578
- if (patientData.patientAge) metaParts.push(String(patientData.patientAge));
1579
- if (patientData.patientSex) metaParts.push(String(patientData.patientSex));
1580
- if (patientData.studyType) metaParts.push(patientData.studyType);
1581
- if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1609
+ if (isSupportChannel) {
1610
+ metaParts.push("Help and support");
1611
+ }
1612
+ if (!isSupportChannel && patientData.patientAge) metaParts.push(String(patientData.patientAge));
1613
+ if (!isSupportChannel && patientData.patientSex) metaParts.push(String(patientData.patientSex));
1614
+ if (!isSupportChannel && patientData.studyType) metaParts.push(patientData.studyType);
1615
+ if (!isSupportChannel && patientData.bodyParts && patientData.bodyParts.length > 0) {
1582
1616
  metaParts.push(patientData.bodyParts.join(", "));
1583
1617
  }
1584
1618
  const hasPrimaryActions = hasDicom && onOpenDicom || onOpenCase;
@@ -4535,6 +4569,7 @@ var DARK_THEME_OVERRIDES = {
4535
4569
  };
4536
4570
  function CollabPanel({
4537
4571
  orderId,
4572
+ conversation: providedConversation,
4538
4573
  patientData,
4539
4574
  participantIds,
4540
4575
  kind,
@@ -4576,7 +4611,13 @@ function CollabPanel({
4576
4611
  loadMoreMessages,
4577
4612
  pinMessage,
4578
4613
  unpinMessage
4579
- } = useConversation({ orderId, patientData, participantIds, kind });
4614
+ } = useConversation({
4615
+ orderId,
4616
+ conversation: providedConversation,
4617
+ patientData,
4618
+ participantIds,
4619
+ kind
4620
+ });
4580
4621
  React4.useEffect(() => {
4581
4622
  onConversationChange?.(conversation);
4582
4623
  }, [conversation, onConversationChange]);
@@ -4585,11 +4626,11 @@ function CollabPanel({
4585
4626
  });
4586
4627
  const { handleDeepLink } = useDeepLinks();
4587
4628
  const handleOpenDicom = () => {
4588
- if (patientData.studyId && patientData.storageId && config.onOpenDicom) {
4629
+ if (patientData?.studyId && patientData.storageId && config.onOpenDicom) {
4589
4630
  config.onOpenDicom(patientData.studyId, patientData.storageId);
4590
4631
  }
4591
4632
  };
4592
- const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
4633
+ const handleOpenCase = !hideOpenCase && config.onOpenCase && patientData?.orderId ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
4593
4634
  const handleJumpToMessage = (messageId) => {
4594
4635
  messageListRef.current?.scrollToMessage(messageId);
4595
4636
  };
@@ -4662,7 +4703,8 @@ function CollabPanel({
4662
4703
  /* @__PURE__ */ jsxRuntime.jsx(
4663
4704
  PatientHeader,
4664
4705
  {
4665
- patientData,
4706
+ patientData: patientData ?? { orderId: "", patientName: conversation?.name ?? "Natoe Support" },
4707
+ isSupportChannel: !patientData,
4666
4708
  participants,
4667
4709
  onOpenDicom: handleOpenDicom,
4668
4710
  onOpenCase: handleOpenCase,
@@ -4793,6 +4835,7 @@ ensureGlobalStyles();
4793
4835
  var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
4794
4836
  function CollabPopup({
4795
4837
  orderId,
4838
+ conversation: providedConversation,
4796
4839
  patientData,
4797
4840
  participantIds,
4798
4841
  kind,
@@ -4819,7 +4862,7 @@ function CollabPopup({
4819
4862
  const containerRef = React4.useRef(null);
4820
4863
  const previouslyFocused = React4.useRef(null);
4821
4864
  const titleId = React4.useId();
4822
- const titleText = cleanChannelName(loadedConversation?.name) || buildChannelName(patientData);
4865
+ const titleText = cleanChannelName(loadedConversation?.name) || (patientData ? buildChannelName(patientData) : "Natoe Support");
4823
4866
  const handleMouseDown = React4.useCallback(
4824
4867
  (e) => {
4825
4868
  if (!e.target.closest("[data-drag-handle]")) return;
@@ -4924,8 +4967,9 @@ function CollabPopup({
4924
4967
  }
4925
4968
  ),
4926
4969
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleCenter, children: [
4927
- /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, style: styles13.patientName, children: patientData.patientName || titleText }),
4970
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, style: styles13.patientName, children: patientData?.patientName || titleText }),
4928
4971
  (() => {
4972
+ if (!patientData) return /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.subRow, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.metaText, children: "Help and support" }) });
4929
4973
  const parts = [];
4930
4974
  if (patientData.patientAge) parts.push(String(patientData.patientAge));
4931
4975
  if (patientData.patientSex) parts.push(String(patientData.patientSex));
@@ -4982,6 +5026,7 @@ function CollabPopup({
4982
5026
  CollabPanel,
4983
5027
  {
4984
5028
  orderId,
5029
+ conversation: providedConversation,
4985
5030
  patientData,
4986
5031
  participantIds,
4987
5032
  kind,
@@ -5901,7 +5946,10 @@ function useConversationList(options) {
5901
5946
 
5902
5947
  // src/core/types.ts
5903
5948
  function isHelpKind(kind) {
5904
- return kind === "help_lab" || kind === "help_radiologist";
5949
+ return kind === "help_lab";
5950
+ }
5951
+ function isCaseKind(kind) {
5952
+ return kind === void 0 || kind === "case";
5905
5953
  }
5906
5954
  function ConversationListItem({
5907
5955
  item,
@@ -6537,6 +6585,7 @@ function CollabInbox({
6537
6585
  );
6538
6586
  }
6539
6587
  function buildPatientData(item) {
6588
+ if (isHelpKind(item.kind) || !item.orderId) return void 0;
6540
6589
  const parsed = parseChannelName(item.name, item.orderId);
6541
6590
  if (item.patientSnapshot) {
6542
6591
  return {
@@ -6874,6 +6923,7 @@ exports.THEME_DEFAULTS = THEME_DEFAULTS;
6874
6923
  exports.THEME_VAR = THEME_VAR;
6875
6924
  exports.TYPING_DEBOUNCE_MS = TYPING_DEBOUNCE_MS;
6876
6925
  exports.applyThemeOverrides = applyThemeOverrides;
6926
+ exports.isCaseKind = isCaseKind;
6877
6927
  exports.isHelpKind = isHelpKind;
6878
6928
  exports.useAudioRecorder = useAudioRecorder;
6879
6929
  exports.useChannelSettings = useChannelSettings;