@natoe/colab 0.1.13 → 0.1.16
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 +120 -14
- package/dist/index.d.ts +120 -14
- package/dist/index.js +317 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +317 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,17 @@ declare const THEME_VAR: {
|
|
|
15
15
|
readonly dangerBorder: "--natoe-colab-danger-border";
|
|
16
16
|
readonly dangerFg: "--natoe-colab-danger-fg";
|
|
17
17
|
readonly fontStack: "--natoe-colab-font-stack";
|
|
18
|
+
readonly white: "--natoe-colab-white";
|
|
19
|
+
readonly neutral50: "--natoe-colab-neutral-50";
|
|
20
|
+
readonly neutral100: "--natoe-colab-neutral-100";
|
|
21
|
+
readonly neutral200: "--natoe-colab-neutral-200";
|
|
22
|
+
readonly neutral300: "--natoe-colab-neutral-300";
|
|
23
|
+
readonly neutral400: "--natoe-colab-neutral-400";
|
|
24
|
+
readonly neutral500: "--natoe-colab-neutral-500";
|
|
25
|
+
readonly neutral600: "--natoe-colab-neutral-600";
|
|
26
|
+
readonly neutral700: "--natoe-colab-neutral-700";
|
|
27
|
+
readonly neutral800: "--natoe-colab-neutral-800";
|
|
28
|
+
readonly neutral900: "--natoe-colab-neutral-900";
|
|
18
29
|
};
|
|
19
30
|
/** Default values for the themeable tokens (used by both CSS emission and
|
|
20
31
|
* the inline-style fallback inside `var(..., fallback)`). */
|
|
@@ -30,6 +41,17 @@ declare const THEME_DEFAULTS: {
|
|
|
30
41
|
readonly dangerBorder: "#fecaca";
|
|
31
42
|
readonly dangerFg: "#b91c1c";
|
|
32
43
|
readonly fontStack: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif";
|
|
44
|
+
readonly white: "#ffffff";
|
|
45
|
+
readonly neutral50: "#f9fafb";
|
|
46
|
+
readonly neutral100: "#f3f4f6";
|
|
47
|
+
readonly neutral200: "#e5e7eb";
|
|
48
|
+
readonly neutral300: "#d1d5db";
|
|
49
|
+
readonly neutral400: "#9ca3af";
|
|
50
|
+
readonly neutral500: "#6b7280";
|
|
51
|
+
readonly neutral600: "#4b5563";
|
|
52
|
+
readonly neutral700: "#374151";
|
|
53
|
+
readonly neutral800: "#1f2937";
|
|
54
|
+
readonly neutral900: "#111827";
|
|
33
55
|
};
|
|
34
56
|
/** Public override shape — passed via CollabConfig.theme. Every key is
|
|
35
57
|
* optional; unspecified keys fall back to {@link THEME_DEFAULTS}. */
|
|
@@ -241,6 +263,14 @@ interface CollabPanelProps {
|
|
|
241
263
|
* relying on potentially-incomplete patientData.
|
|
242
264
|
*/
|
|
243
265
|
onConversationChange?: (conversation: Conversation | null) => void;
|
|
266
|
+
/**
|
|
267
|
+
* Color mode for the entire panel. Default 'light'. 'dark' is meant for
|
|
268
|
+
* embedding inside the viewer's left panel (#000 background) — applies
|
|
269
|
+
* dark overrides for the themeable CSS variables on the panel root, so
|
|
270
|
+
* every internal surface (PatientHeader, MessageList, MessageBubble,
|
|
271
|
+
* MessageInput, PinnedMessagesBar, etc.) inherits them via cascade.
|
|
272
|
+
*/
|
|
273
|
+
themeMode?: 'light' | 'dark';
|
|
244
274
|
/** Custom class name for the outer container */
|
|
245
275
|
className?: string;
|
|
246
276
|
/** Custom inline styles for the outer container */
|
|
@@ -250,7 +280,7 @@ interface CollabPanelProps {
|
|
|
250
280
|
* Full collaboration panel: patient header + pinned bar + message thread + input.
|
|
251
281
|
* Supports reply, pin/unpin, and (optionally) seen-by indicators.
|
|
252
282
|
*/
|
|
253
|
-
declare function CollabPanel({ orderId, patientData, participantIds, showSeenBy, onBack, hidePatientName, onConversationChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
|
|
283
|
+
declare function CollabPanel({ orderId, patientData, participantIds, showSeenBy, onBack, hidePatientName, onConversationChange, themeMode, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
|
|
254
284
|
|
|
255
285
|
interface CollabPopupProps {
|
|
256
286
|
/** Order ID for this conversation */
|
|
@@ -290,6 +320,7 @@ interface CollabPopupProps {
|
|
|
290
320
|
*/
|
|
291
321
|
declare function CollabPopup({ orderId, patientData, participantIds, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
|
|
292
322
|
|
|
323
|
+
type ColabInlineMode = 'light' | 'dark';
|
|
293
324
|
interface CollabInlineProps {
|
|
294
325
|
orderId: string;
|
|
295
326
|
patientData: PatientData;
|
|
@@ -297,24 +328,32 @@ interface CollabInlineProps {
|
|
|
297
328
|
participantIds?: string[];
|
|
298
329
|
/** Called when user clicks the expand button — host opens the floating popup */
|
|
299
330
|
onExpand?: () => void;
|
|
300
|
-
/** Max inline messages to show (default
|
|
331
|
+
/** Max inline messages to show (default 1 — just the latest) */
|
|
301
332
|
messageLimit?: number;
|
|
302
333
|
/** Placeholder text for the input */
|
|
303
334
|
placeholder?: string;
|
|
335
|
+
/**
|
|
336
|
+
* Color mode for the inline surface. Defaults to 'light'. 'dark' is meant
|
|
337
|
+
* for embedding inside the viewer's left panel (black background) — only
|
|
338
|
+
* neutral/structural colors switch; brand/role/semantic colors stay the
|
|
339
|
+
* same in both modes.
|
|
340
|
+
*/
|
|
341
|
+
mode?: ColabInlineMode;
|
|
304
342
|
className?: string;
|
|
305
343
|
style?: React.CSSProperties;
|
|
306
344
|
}
|
|
307
345
|
/**
|
|
308
|
-
* Inline chat for table rows.
|
|
346
|
+
* Inline chat for table rows or side panels.
|
|
309
347
|
*
|
|
310
348
|
* Two states:
|
|
311
349
|
* - No conversation yet: just an input bar (no socket, no preview)
|
|
312
|
-
* - Conversation exists: last N
|
|
350
|
+
* - Conversation exists: latest message (or last N if messageLimit > 1)
|
|
351
|
+
* + input + expand button
|
|
313
352
|
*
|
|
314
353
|
* Supports text and voice messages. File attachments and full chat features
|
|
315
354
|
* stay in the floating popup / inbox.
|
|
316
355
|
*/
|
|
317
|
-
declare function CollabInline({ orderId, patientData, participantIds, onExpand, messageLimit, placeholder, className, style, }: CollabInlineProps): react_jsx_runtime.JSX.Element;
|
|
356
|
+
declare function CollabInline({ orderId, patientData, participantIds, onExpand, messageLimit, placeholder, mode, className, style, }: CollabInlineProps): react_jsx_runtime.JSX.Element;
|
|
318
357
|
|
|
319
358
|
interface CollabInboxProps {
|
|
320
359
|
/** Optional: preselect a specific conversation (by conversation ID) */
|
|
@@ -341,6 +380,20 @@ type PresenceCallback = (presences: Record<string, {
|
|
|
341
380
|
userId: string;
|
|
342
381
|
isOnline: boolean;
|
|
343
382
|
}>) => void;
|
|
383
|
+
/**
|
|
384
|
+
* Server-pushed unread counts. The backend broadcasts BOTH keyings in
|
|
385
|
+
* one payload so consumers can render badges at either granularity (FAB
|
|
386
|
+
* + inbox row use `conversation`; table-row chat icons use `order`) off
|
|
387
|
+
* a single socket event. Legacy backends that send a flat
|
|
388
|
+
* `Record<string, number>` are normalised at the socket boundary into
|
|
389
|
+
* `{ conversation: <flat>, order: {} }` for forward compatibility.
|
|
390
|
+
*/
|
|
391
|
+
interface UnreadCounts {
|
|
392
|
+
/** keyed by conversation id */
|
|
393
|
+
conversation: Record<string, number>;
|
|
394
|
+
/** keyed by order id — used for table-row chat-icon badges */
|
|
395
|
+
order: Record<string, number>;
|
|
396
|
+
}
|
|
344
397
|
interface ConversationCallbacks {
|
|
345
398
|
onMessage?: MessageCallback;
|
|
346
399
|
onTyping?: TypingCallback;
|
|
@@ -358,12 +411,31 @@ interface ConversationCallbacks {
|
|
|
358
411
|
messageId: string;
|
|
359
412
|
}) => void;
|
|
360
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Returned by `joinConversation()`. Each caller gets its own handle and
|
|
416
|
+
* its own bound listeners — calling `release()` unbinds only THIS
|
|
417
|
+
* subscriber's listeners. The channel only actually leaves the server
|
|
418
|
+
* when the last subscriber releases.
|
|
419
|
+
*/
|
|
420
|
+
interface ChannelSubscription {
|
|
421
|
+
/** Underlying Phoenix channel — callers that need to push/observe directly. */
|
|
422
|
+
channel: Channel;
|
|
423
|
+
/** Drop this subscriber. Idempotent — safe to call twice. */
|
|
424
|
+
release: () => void;
|
|
425
|
+
}
|
|
361
426
|
/**
|
|
362
427
|
* Manages Phoenix WebSocket connection and channel subscriptions.
|
|
363
428
|
* One instance per authenticated user session.
|
|
364
429
|
*/
|
|
365
430
|
declare class CollabSocket {
|
|
366
431
|
private socket;
|
|
432
|
+
/**
|
|
433
|
+
* Conversation channels keyed by id. Each entry is reference-counted so
|
|
434
|
+
* multiple surfaces (e.g. inline chat + expanded panel mounted at once
|
|
435
|
+
* for the same conversation) can coexist without one's `leaveConversation`
|
|
436
|
+
* tearing the channel out from under the other. See `joinConversation`
|
|
437
|
+
* and the returned `ChannelSubscription.release`.
|
|
438
|
+
*/
|
|
367
439
|
private channels;
|
|
368
440
|
private presences;
|
|
369
441
|
private userChannel;
|
|
@@ -375,12 +447,34 @@ declare class CollabSocket {
|
|
|
375
447
|
connect(config: CollabConfig): void;
|
|
376
448
|
/** Subscribe to user-level events (unread counts, notifications) */
|
|
377
449
|
private joinUserChannel;
|
|
378
|
-
/** Register callback for unread count changes
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
|
|
450
|
+
/** Register callback for unread count changes. The callback receives
|
|
451
|
+
* the normalised {@link UnreadCounts} shape regardless of which
|
|
452
|
+
* payload format the backend pushed. */
|
|
453
|
+
onUnreadCountUpdate(callback: (counts: UnreadCounts) => void): void;
|
|
454
|
+
/**
|
|
455
|
+
* Join a conversation channel and subscribe to events.
|
|
456
|
+
*
|
|
457
|
+
* Reference-counted: multiple callers can join the same conversation
|
|
458
|
+
* (e.g. inline preview + expanded panel mounted side-by-side). Each
|
|
459
|
+
* call binds its own listeners and gets back a `ChannelSubscription`.
|
|
460
|
+
* The underlying channel only `.leave()`s the server when the LAST
|
|
461
|
+
* subscriber calls `release()`.
|
|
462
|
+
*/
|
|
463
|
+
joinConversation(conversationId: string, callbacks: ConversationCallbacks): ChannelSubscription | null;
|
|
464
|
+
/**
|
|
465
|
+
* @deprecated Use the `release()` method returned by `joinConversation()`.
|
|
466
|
+
* Kept as a no-op so older callers don't throw — but it cannot identify
|
|
467
|
+
* which subscriber should leave, so it silently does nothing. Any code
|
|
468
|
+
* still calling this will leak listeners and prevent the channel from
|
|
469
|
+
* ever being torn down. Migrate to the subscription handle.
|
|
470
|
+
*/
|
|
471
|
+
leaveConversation(_conversationId: string): void;
|
|
472
|
+
/**
|
|
473
|
+
* Look up the underlying Phoenix Channel for a conversation, if any
|
|
474
|
+
* subscriber is still holding it. All send/push paths go through this
|
|
475
|
+
* helper so the refcounted entry shape is contained to joinConversation.
|
|
476
|
+
*/
|
|
477
|
+
private getChannel;
|
|
384
478
|
/** Send a message to a conversation.
|
|
385
479
|
*
|
|
386
480
|
* Phoenix buffers pushes fired while a channel is still joining and
|
|
@@ -434,6 +528,13 @@ interface CollabContextValue {
|
|
|
434
528
|
* mounting their own useConversation.
|
|
435
529
|
*/
|
|
436
530
|
unreadCounts: Record<string, number>;
|
|
531
|
+
/**
|
|
532
|
+
* Same unread counts, but keyed by **order ID** instead of
|
|
533
|
+
* conversation ID. Useful for table-row chat-icon badges where the
|
|
534
|
+
* row only knows its order_id, not the conversation_id. Same socket
|
|
535
|
+
* push updates both maps in lock-step.
|
|
536
|
+
*/
|
|
537
|
+
unreadCountsByOrder: Record<string, number>;
|
|
437
538
|
/** Batched preview lookup — coalesces calls within a microtask */
|
|
438
539
|
requestPreview: (orderId: string) => Promise<ConversationPreview | null>;
|
|
439
540
|
/** Invalidate a cached preview (e.g. when a new message arrives) */
|
|
@@ -716,8 +817,13 @@ interface ConversationListProps {
|
|
|
716
817
|
/**
|
|
717
818
|
* Scrollable inbox of conversations. Unread/All tabs (with count pills)
|
|
718
819
|
* sit at the top, followed by a search field with a focus-within ring.
|
|
719
|
-
*
|
|
720
|
-
*
|
|
820
|
+
* The inbox lands on Unread by default whenever there are any unread
|
|
821
|
+
* conversations — the initial choice reads CollabProvider's socket-
|
|
822
|
+
* pushed `totalUnread`, which is already populated by the time the
|
|
823
|
+
* FAB is clicked (it drives the badge), so there's no "All flash"
|
|
824
|
+
* during the conversation-list fetch. A fallback effect handles the
|
|
825
|
+
* edge case where the conversation list loads with unread items but
|
|
826
|
+
* `totalUnread` was still zero at mount time. Empty/loading/error
|
|
721
827
|
* states are illustrated and descriptive rather than terse one-liners.
|
|
722
828
|
*/
|
|
723
829
|
declare function ConversationList({ conversations, selectedId, isLoading, error, onSelect, className, }: ConversationListProps): react_jsx_runtime.JSX.Element;
|
|
@@ -774,7 +880,7 @@ interface PatientHeaderProps {
|
|
|
774
880
|
* experience and there's no outer title, so we still show the name +
|
|
775
881
|
* back button stacked above the case card.
|
|
776
882
|
*/
|
|
777
|
-
declare function PatientHeader({ patientData, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
|
|
883
|
+
declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
|
|
778
884
|
|
|
779
885
|
interface MessageListHandle {
|
|
780
886
|
/** Scroll to a specific message by ID (used by pin jump-to) */
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ declare const THEME_VAR: {
|
|
|
15
15
|
readonly dangerBorder: "--natoe-colab-danger-border";
|
|
16
16
|
readonly dangerFg: "--natoe-colab-danger-fg";
|
|
17
17
|
readonly fontStack: "--natoe-colab-font-stack";
|
|
18
|
+
readonly white: "--natoe-colab-white";
|
|
19
|
+
readonly neutral50: "--natoe-colab-neutral-50";
|
|
20
|
+
readonly neutral100: "--natoe-colab-neutral-100";
|
|
21
|
+
readonly neutral200: "--natoe-colab-neutral-200";
|
|
22
|
+
readonly neutral300: "--natoe-colab-neutral-300";
|
|
23
|
+
readonly neutral400: "--natoe-colab-neutral-400";
|
|
24
|
+
readonly neutral500: "--natoe-colab-neutral-500";
|
|
25
|
+
readonly neutral600: "--natoe-colab-neutral-600";
|
|
26
|
+
readonly neutral700: "--natoe-colab-neutral-700";
|
|
27
|
+
readonly neutral800: "--natoe-colab-neutral-800";
|
|
28
|
+
readonly neutral900: "--natoe-colab-neutral-900";
|
|
18
29
|
};
|
|
19
30
|
/** Default values for the themeable tokens (used by both CSS emission and
|
|
20
31
|
* the inline-style fallback inside `var(..., fallback)`). */
|
|
@@ -30,6 +41,17 @@ declare const THEME_DEFAULTS: {
|
|
|
30
41
|
readonly dangerBorder: "#fecaca";
|
|
31
42
|
readonly dangerFg: "#b91c1c";
|
|
32
43
|
readonly fontStack: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif";
|
|
44
|
+
readonly white: "#ffffff";
|
|
45
|
+
readonly neutral50: "#f9fafb";
|
|
46
|
+
readonly neutral100: "#f3f4f6";
|
|
47
|
+
readonly neutral200: "#e5e7eb";
|
|
48
|
+
readonly neutral300: "#d1d5db";
|
|
49
|
+
readonly neutral400: "#9ca3af";
|
|
50
|
+
readonly neutral500: "#6b7280";
|
|
51
|
+
readonly neutral600: "#4b5563";
|
|
52
|
+
readonly neutral700: "#374151";
|
|
53
|
+
readonly neutral800: "#1f2937";
|
|
54
|
+
readonly neutral900: "#111827";
|
|
33
55
|
};
|
|
34
56
|
/** Public override shape — passed via CollabConfig.theme. Every key is
|
|
35
57
|
* optional; unspecified keys fall back to {@link THEME_DEFAULTS}. */
|
|
@@ -241,6 +263,14 @@ interface CollabPanelProps {
|
|
|
241
263
|
* relying on potentially-incomplete patientData.
|
|
242
264
|
*/
|
|
243
265
|
onConversationChange?: (conversation: Conversation | null) => void;
|
|
266
|
+
/**
|
|
267
|
+
* Color mode for the entire panel. Default 'light'. 'dark' is meant for
|
|
268
|
+
* embedding inside the viewer's left panel (#000 background) — applies
|
|
269
|
+
* dark overrides for the themeable CSS variables on the panel root, so
|
|
270
|
+
* every internal surface (PatientHeader, MessageList, MessageBubble,
|
|
271
|
+
* MessageInput, PinnedMessagesBar, etc.) inherits them via cascade.
|
|
272
|
+
*/
|
|
273
|
+
themeMode?: 'light' | 'dark';
|
|
244
274
|
/** Custom class name for the outer container */
|
|
245
275
|
className?: string;
|
|
246
276
|
/** Custom inline styles for the outer container */
|
|
@@ -250,7 +280,7 @@ interface CollabPanelProps {
|
|
|
250
280
|
* Full collaboration panel: patient header + pinned bar + message thread + input.
|
|
251
281
|
* Supports reply, pin/unpin, and (optionally) seen-by indicators.
|
|
252
282
|
*/
|
|
253
|
-
declare function CollabPanel({ orderId, patientData, participantIds, showSeenBy, onBack, hidePatientName, onConversationChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
|
|
283
|
+
declare function CollabPanel({ orderId, patientData, participantIds, showSeenBy, onBack, hidePatientName, onConversationChange, themeMode, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
|
|
254
284
|
|
|
255
285
|
interface CollabPopupProps {
|
|
256
286
|
/** Order ID for this conversation */
|
|
@@ -290,6 +320,7 @@ interface CollabPopupProps {
|
|
|
290
320
|
*/
|
|
291
321
|
declare function CollabPopup({ orderId, patientData, participantIds, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
|
|
292
322
|
|
|
323
|
+
type ColabInlineMode = 'light' | 'dark';
|
|
293
324
|
interface CollabInlineProps {
|
|
294
325
|
orderId: string;
|
|
295
326
|
patientData: PatientData;
|
|
@@ -297,24 +328,32 @@ interface CollabInlineProps {
|
|
|
297
328
|
participantIds?: string[];
|
|
298
329
|
/** Called when user clicks the expand button — host opens the floating popup */
|
|
299
330
|
onExpand?: () => void;
|
|
300
|
-
/** Max inline messages to show (default
|
|
331
|
+
/** Max inline messages to show (default 1 — just the latest) */
|
|
301
332
|
messageLimit?: number;
|
|
302
333
|
/** Placeholder text for the input */
|
|
303
334
|
placeholder?: string;
|
|
335
|
+
/**
|
|
336
|
+
* Color mode for the inline surface. Defaults to 'light'. 'dark' is meant
|
|
337
|
+
* for embedding inside the viewer's left panel (black background) — only
|
|
338
|
+
* neutral/structural colors switch; brand/role/semantic colors stay the
|
|
339
|
+
* same in both modes.
|
|
340
|
+
*/
|
|
341
|
+
mode?: ColabInlineMode;
|
|
304
342
|
className?: string;
|
|
305
343
|
style?: React.CSSProperties;
|
|
306
344
|
}
|
|
307
345
|
/**
|
|
308
|
-
* Inline chat for table rows.
|
|
346
|
+
* Inline chat for table rows or side panels.
|
|
309
347
|
*
|
|
310
348
|
* Two states:
|
|
311
349
|
* - No conversation yet: just an input bar (no socket, no preview)
|
|
312
|
-
* - Conversation exists: last N
|
|
350
|
+
* - Conversation exists: latest message (or last N if messageLimit > 1)
|
|
351
|
+
* + input + expand button
|
|
313
352
|
*
|
|
314
353
|
* Supports text and voice messages. File attachments and full chat features
|
|
315
354
|
* stay in the floating popup / inbox.
|
|
316
355
|
*/
|
|
317
|
-
declare function CollabInline({ orderId, patientData, participantIds, onExpand, messageLimit, placeholder, className, style, }: CollabInlineProps): react_jsx_runtime.JSX.Element;
|
|
356
|
+
declare function CollabInline({ orderId, patientData, participantIds, onExpand, messageLimit, placeholder, mode, className, style, }: CollabInlineProps): react_jsx_runtime.JSX.Element;
|
|
318
357
|
|
|
319
358
|
interface CollabInboxProps {
|
|
320
359
|
/** Optional: preselect a specific conversation (by conversation ID) */
|
|
@@ -341,6 +380,20 @@ type PresenceCallback = (presences: Record<string, {
|
|
|
341
380
|
userId: string;
|
|
342
381
|
isOnline: boolean;
|
|
343
382
|
}>) => void;
|
|
383
|
+
/**
|
|
384
|
+
* Server-pushed unread counts. The backend broadcasts BOTH keyings in
|
|
385
|
+
* one payload so consumers can render badges at either granularity (FAB
|
|
386
|
+
* + inbox row use `conversation`; table-row chat icons use `order`) off
|
|
387
|
+
* a single socket event. Legacy backends that send a flat
|
|
388
|
+
* `Record<string, number>` are normalised at the socket boundary into
|
|
389
|
+
* `{ conversation: <flat>, order: {} }` for forward compatibility.
|
|
390
|
+
*/
|
|
391
|
+
interface UnreadCounts {
|
|
392
|
+
/** keyed by conversation id */
|
|
393
|
+
conversation: Record<string, number>;
|
|
394
|
+
/** keyed by order id — used for table-row chat-icon badges */
|
|
395
|
+
order: Record<string, number>;
|
|
396
|
+
}
|
|
344
397
|
interface ConversationCallbacks {
|
|
345
398
|
onMessage?: MessageCallback;
|
|
346
399
|
onTyping?: TypingCallback;
|
|
@@ -358,12 +411,31 @@ interface ConversationCallbacks {
|
|
|
358
411
|
messageId: string;
|
|
359
412
|
}) => void;
|
|
360
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Returned by `joinConversation()`. Each caller gets its own handle and
|
|
416
|
+
* its own bound listeners — calling `release()` unbinds only THIS
|
|
417
|
+
* subscriber's listeners. The channel only actually leaves the server
|
|
418
|
+
* when the last subscriber releases.
|
|
419
|
+
*/
|
|
420
|
+
interface ChannelSubscription {
|
|
421
|
+
/** Underlying Phoenix channel — callers that need to push/observe directly. */
|
|
422
|
+
channel: Channel;
|
|
423
|
+
/** Drop this subscriber. Idempotent — safe to call twice. */
|
|
424
|
+
release: () => void;
|
|
425
|
+
}
|
|
361
426
|
/**
|
|
362
427
|
* Manages Phoenix WebSocket connection and channel subscriptions.
|
|
363
428
|
* One instance per authenticated user session.
|
|
364
429
|
*/
|
|
365
430
|
declare class CollabSocket {
|
|
366
431
|
private socket;
|
|
432
|
+
/**
|
|
433
|
+
* Conversation channels keyed by id. Each entry is reference-counted so
|
|
434
|
+
* multiple surfaces (e.g. inline chat + expanded panel mounted at once
|
|
435
|
+
* for the same conversation) can coexist without one's `leaveConversation`
|
|
436
|
+
* tearing the channel out from under the other. See `joinConversation`
|
|
437
|
+
* and the returned `ChannelSubscription.release`.
|
|
438
|
+
*/
|
|
367
439
|
private channels;
|
|
368
440
|
private presences;
|
|
369
441
|
private userChannel;
|
|
@@ -375,12 +447,34 @@ declare class CollabSocket {
|
|
|
375
447
|
connect(config: CollabConfig): void;
|
|
376
448
|
/** Subscribe to user-level events (unread counts, notifications) */
|
|
377
449
|
private joinUserChannel;
|
|
378
|
-
/** Register callback for unread count changes
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
|
|
450
|
+
/** Register callback for unread count changes. The callback receives
|
|
451
|
+
* the normalised {@link UnreadCounts} shape regardless of which
|
|
452
|
+
* payload format the backend pushed. */
|
|
453
|
+
onUnreadCountUpdate(callback: (counts: UnreadCounts) => void): void;
|
|
454
|
+
/**
|
|
455
|
+
* Join a conversation channel and subscribe to events.
|
|
456
|
+
*
|
|
457
|
+
* Reference-counted: multiple callers can join the same conversation
|
|
458
|
+
* (e.g. inline preview + expanded panel mounted side-by-side). Each
|
|
459
|
+
* call binds its own listeners and gets back a `ChannelSubscription`.
|
|
460
|
+
* The underlying channel only `.leave()`s the server when the LAST
|
|
461
|
+
* subscriber calls `release()`.
|
|
462
|
+
*/
|
|
463
|
+
joinConversation(conversationId: string, callbacks: ConversationCallbacks): ChannelSubscription | null;
|
|
464
|
+
/**
|
|
465
|
+
* @deprecated Use the `release()` method returned by `joinConversation()`.
|
|
466
|
+
* Kept as a no-op so older callers don't throw — but it cannot identify
|
|
467
|
+
* which subscriber should leave, so it silently does nothing. Any code
|
|
468
|
+
* still calling this will leak listeners and prevent the channel from
|
|
469
|
+
* ever being torn down. Migrate to the subscription handle.
|
|
470
|
+
*/
|
|
471
|
+
leaveConversation(_conversationId: string): void;
|
|
472
|
+
/**
|
|
473
|
+
* Look up the underlying Phoenix Channel for a conversation, if any
|
|
474
|
+
* subscriber is still holding it. All send/push paths go through this
|
|
475
|
+
* helper so the refcounted entry shape is contained to joinConversation.
|
|
476
|
+
*/
|
|
477
|
+
private getChannel;
|
|
384
478
|
/** Send a message to a conversation.
|
|
385
479
|
*
|
|
386
480
|
* Phoenix buffers pushes fired while a channel is still joining and
|
|
@@ -434,6 +528,13 @@ interface CollabContextValue {
|
|
|
434
528
|
* mounting their own useConversation.
|
|
435
529
|
*/
|
|
436
530
|
unreadCounts: Record<string, number>;
|
|
531
|
+
/**
|
|
532
|
+
* Same unread counts, but keyed by **order ID** instead of
|
|
533
|
+
* conversation ID. Useful for table-row chat-icon badges where the
|
|
534
|
+
* row only knows its order_id, not the conversation_id. Same socket
|
|
535
|
+
* push updates both maps in lock-step.
|
|
536
|
+
*/
|
|
537
|
+
unreadCountsByOrder: Record<string, number>;
|
|
437
538
|
/** Batched preview lookup — coalesces calls within a microtask */
|
|
438
539
|
requestPreview: (orderId: string) => Promise<ConversationPreview | null>;
|
|
439
540
|
/** Invalidate a cached preview (e.g. when a new message arrives) */
|
|
@@ -716,8 +817,13 @@ interface ConversationListProps {
|
|
|
716
817
|
/**
|
|
717
818
|
* Scrollable inbox of conversations. Unread/All tabs (with count pills)
|
|
718
819
|
* sit at the top, followed by a search field with a focus-within ring.
|
|
719
|
-
*
|
|
720
|
-
*
|
|
820
|
+
* The inbox lands on Unread by default whenever there are any unread
|
|
821
|
+
* conversations — the initial choice reads CollabProvider's socket-
|
|
822
|
+
* pushed `totalUnread`, which is already populated by the time the
|
|
823
|
+
* FAB is clicked (it drives the badge), so there's no "All flash"
|
|
824
|
+
* during the conversation-list fetch. A fallback effect handles the
|
|
825
|
+
* edge case where the conversation list loads with unread items but
|
|
826
|
+
* `totalUnread` was still zero at mount time. Empty/loading/error
|
|
721
827
|
* states are illustrated and descriptive rather than terse one-liners.
|
|
722
828
|
*/
|
|
723
829
|
declare function ConversationList({ conversations, selectedId, isLoading, error, onSelect, className, }: ConversationListProps): react_jsx_runtime.JSX.Element;
|
|
@@ -774,7 +880,7 @@ interface PatientHeaderProps {
|
|
|
774
880
|
* experience and there's no outer title, so we still show the name +
|
|
775
881
|
* back button stacked above the case card.
|
|
776
882
|
*/
|
|
777
|
-
declare function PatientHeader({ patientData, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
|
|
883
|
+
declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
|
|
778
884
|
|
|
779
885
|
interface MessageListHandle {
|
|
780
886
|
/** Scroll to a specific message by ID (used by pin jump-to) */
|