@natoe/colab 0.1.26 → 0.1.28
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 +52 -9
- package/dist/index.d.ts +52 -9
- package/dist/index.js +132 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -582,6 +582,16 @@ function applyThemeOverrides(theme) {
|
|
|
582
582
|
}
|
|
583
583
|
});
|
|
584
584
|
}
|
|
585
|
+
function previewKey(orderId, kind) {
|
|
586
|
+
return `${kind}|${orderId}`;
|
|
587
|
+
}
|
|
588
|
+
function parsePreviewKey(key) {
|
|
589
|
+
const separator = key.indexOf("|");
|
|
590
|
+
return {
|
|
591
|
+
kind: key.slice(0, separator),
|
|
592
|
+
orderId: key.slice(separator + 1)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
585
595
|
var CollabContext = React4.createContext(null);
|
|
586
596
|
function useCollab() {
|
|
587
597
|
const context = React4.useContext(CollabContext);
|
|
@@ -597,7 +607,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
597
607
|
});
|
|
598
608
|
const [unreadCounts, setUnreadCounts] = React4.useState({});
|
|
599
609
|
const [unreadCountsByOrder, setUnreadCountsByOrder] = React4.useState({});
|
|
600
|
-
const
|
|
610
|
+
const pendingKeys = React4.useRef(/* @__PURE__ */ new Set());
|
|
601
611
|
const pendingResolvers = React4.useRef(/* @__PURE__ */ new Map());
|
|
602
612
|
const previewCache = React4.useRef(/* @__PURE__ */ new Map());
|
|
603
613
|
const batchScheduled = React4.useRef(false);
|
|
@@ -628,47 +638,61 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
628
638
|
[config]
|
|
629
639
|
);
|
|
630
640
|
const flushPreviewBatch = React4.useCallback(async () => {
|
|
631
|
-
const
|
|
641
|
+
const keys = Array.from(pendingKeys.current);
|
|
632
642
|
const resolvers = new Map(pendingResolvers.current);
|
|
633
|
-
|
|
643
|
+
pendingKeys.current.clear();
|
|
634
644
|
pendingResolvers.current.clear();
|
|
635
645
|
batchScheduled.current = false;
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const data = snakeToCamel(await response.json());
|
|
644
|
-
orderIds.forEach((orderId) => {
|
|
645
|
-
const preview = data.previews[orderId] ?? null;
|
|
646
|
-
if (preview !== null) {
|
|
647
|
-
previewCache.current.set(orderId, preview);
|
|
648
|
-
}
|
|
649
|
-
resolvers.get(orderId)?.forEach((resolve) => resolve(preview));
|
|
650
|
-
});
|
|
651
|
-
} catch (error) {
|
|
652
|
-
config.onError?.({
|
|
653
|
-
code: "PREVIEW_BATCH_ERROR",
|
|
654
|
-
message: "Failed to fetch conversation previews",
|
|
655
|
-
details: error
|
|
656
|
-
});
|
|
657
|
-
orderIds.forEach((orderId) => {
|
|
658
|
-
resolvers.get(orderId)?.forEach((resolve) => resolve(null));
|
|
659
|
-
});
|
|
646
|
+
if (keys.length === 0) return;
|
|
647
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const key of keys) {
|
|
649
|
+
const { orderId, kind } = parsePreviewKey(key);
|
|
650
|
+
const list = byKind.get(kind) ?? [];
|
|
651
|
+
list.push(orderId);
|
|
652
|
+
byKind.set(kind, list);
|
|
660
653
|
}
|
|
654
|
+
await Promise.all(
|
|
655
|
+
Array.from(byKind.entries()).map(async ([kind, orderIds]) => {
|
|
656
|
+
try {
|
|
657
|
+
const query = orderIds.map((id) => `order_ids[]=${encodeURIComponent(id)}`).join("&");
|
|
658
|
+
const response = await fetch(
|
|
659
|
+
`${apiBaseUrl}/api/natoe-colab/conversations/previews?${query}&kind=${encodeURIComponent(kind)}`,
|
|
660
|
+
{ headers: authHeaders() }
|
|
661
|
+
);
|
|
662
|
+
if (!response.ok) throw new Error(`Preview batch failed: ${response.status}`);
|
|
663
|
+
const data = snakeToCamel(await response.json());
|
|
664
|
+
orderIds.forEach((orderId) => {
|
|
665
|
+
const key = previewKey(orderId, kind);
|
|
666
|
+
const preview = data.previews[orderId] ?? null;
|
|
667
|
+
if (preview !== null) {
|
|
668
|
+
previewCache.current.set(key, preview);
|
|
669
|
+
}
|
|
670
|
+
resolvers.get(key)?.forEach((resolve) => resolve(preview));
|
|
671
|
+
});
|
|
672
|
+
} catch (error) {
|
|
673
|
+
config.onError?.({
|
|
674
|
+
code: "PREVIEW_BATCH_ERROR",
|
|
675
|
+
message: "Failed to fetch conversation previews",
|
|
676
|
+
details: error
|
|
677
|
+
});
|
|
678
|
+
orderIds.forEach((orderId) => {
|
|
679
|
+
resolvers.get(previewKey(orderId, kind))?.forEach((resolve) => resolve(null));
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
);
|
|
661
684
|
}, [apiBaseUrl, authHeaders, config]);
|
|
662
685
|
const requestPreview = React4.useCallback(
|
|
663
|
-
(orderId) => {
|
|
664
|
-
|
|
665
|
-
|
|
686
|
+
(orderId, kind = "case") => {
|
|
687
|
+
const key = previewKey(orderId, kind);
|
|
688
|
+
if (previewCache.current.has(key)) {
|
|
689
|
+
return Promise.resolve(previewCache.current.get(key) ?? null);
|
|
666
690
|
}
|
|
667
691
|
return new Promise((resolve) => {
|
|
668
|
-
|
|
669
|
-
const existing = pendingResolvers.current.get(
|
|
692
|
+
pendingKeys.current.add(key);
|
|
693
|
+
const existing = pendingResolvers.current.get(key) || [];
|
|
670
694
|
existing.push(resolve);
|
|
671
|
-
pendingResolvers.current.set(
|
|
695
|
+
pendingResolvers.current.set(key, existing);
|
|
672
696
|
if (!batchScheduled.current) {
|
|
673
697
|
batchScheduled.current = true;
|
|
674
698
|
queueMicrotask(flushPreviewBatch);
|
|
@@ -677,8 +701,16 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
677
701
|
},
|
|
678
702
|
[flushPreviewBatch]
|
|
679
703
|
);
|
|
680
|
-
const invalidatePreview = React4.useCallback((orderId) => {
|
|
681
|
-
|
|
704
|
+
const invalidatePreview = React4.useCallback((orderId, kind) => {
|
|
705
|
+
if (kind) {
|
|
706
|
+
previewCache.current.delete(previewKey(orderId, kind));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
for (const key of Array.from(previewCache.current.keys())) {
|
|
710
|
+
if (parsePreviewKey(key).orderId === orderId) {
|
|
711
|
+
previewCache.current.delete(key);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
682
714
|
}, []);
|
|
683
715
|
const fetchMessages = React4.useCallback(
|
|
684
716
|
async (conversationId, options = {}) => {
|
|
@@ -705,11 +737,17 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
705
737
|
message: "Cannot create conversation: orderId is empty. Host app passed an order with no orderId/mainOrderId set."
|
|
706
738
|
};
|
|
707
739
|
}
|
|
740
|
+
const kind = options.kind ?? "case";
|
|
708
741
|
const response = await fetch(`${apiBaseUrl}/api/natoe-colab/orders/${orderId}/messages`, {
|
|
709
742
|
method: "POST",
|
|
710
743
|
headers: authHeaders(),
|
|
711
744
|
body: JSON.stringify({
|
|
712
745
|
message,
|
|
746
|
+
// Top-level, matching the backend's `params["kind"]`. It selects
|
|
747
|
+
// BOTH which thread is addressed and which authorization path runs
|
|
748
|
+
// — help kinds go through Access.can_access_help?/5, not the
|
|
749
|
+
// broader case-chat check.
|
|
750
|
+
kind,
|
|
713
751
|
conversation: {
|
|
714
752
|
name: options.name,
|
|
715
753
|
participant_ids: options.participantIds ?? []
|
|
@@ -721,7 +759,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
721
759
|
throw { code: "CREATE_ERROR", message: "Failed to create conversation", details: error };
|
|
722
760
|
}
|
|
723
761
|
const data = snakeToCamel(await response.json());
|
|
724
|
-
previewCache.current.delete(orderId);
|
|
762
|
+
previewCache.current.delete(previewKey(orderId, kind));
|
|
725
763
|
return data;
|
|
726
764
|
},
|
|
727
765
|
[apiBaseUrl, authHeaders]
|
|
@@ -855,7 +893,8 @@ function useConversation({
|
|
|
855
893
|
orderId,
|
|
856
894
|
patientData,
|
|
857
895
|
participantIds = [],
|
|
858
|
-
loadHistory = true
|
|
896
|
+
loadHistory = true,
|
|
897
|
+
kind = "case"
|
|
859
898
|
}) {
|
|
860
899
|
const {
|
|
861
900
|
socket,
|
|
@@ -983,7 +1022,7 @@ function useConversation({
|
|
|
983
1022
|
setIsLoading(true);
|
|
984
1023
|
setError(null);
|
|
985
1024
|
try {
|
|
986
|
-
const preview = await requestPreview(orderId);
|
|
1025
|
+
const preview = await requestPreview(orderId, kind);
|
|
987
1026
|
if (cancelled) return;
|
|
988
1027
|
if (!preview) {
|
|
989
1028
|
setConversation(null);
|
|
@@ -1040,7 +1079,7 @@ function useConversation({
|
|
|
1040
1079
|
markedReadIdsRef.current.clear();
|
|
1041
1080
|
setIsConnected(false);
|
|
1042
1081
|
};
|
|
1043
|
-
}, [orderId]);
|
|
1082
|
+
}, [orderId, kind]);
|
|
1044
1083
|
const ensureConversation = React4.useCallback(
|
|
1045
1084
|
async (payload) => {
|
|
1046
1085
|
if (conversation) return { conv: conversation, persistedByCreate: false };
|
|
@@ -1051,13 +1090,14 @@ function useConversation({
|
|
|
1051
1090
|
const inflight = (async () => {
|
|
1052
1091
|
const result = await createConversationWithMessage(orderId, payload, {
|
|
1053
1092
|
name: buildName(),
|
|
1054
|
-
participantIds
|
|
1093
|
+
participantIds,
|
|
1094
|
+
kind
|
|
1055
1095
|
});
|
|
1056
1096
|
setConversation(result.conversation);
|
|
1057
1097
|
setParticipants(result.conversation.participants);
|
|
1058
1098
|
setMessages([result.message]);
|
|
1059
1099
|
joinChannel(result.conversation);
|
|
1060
|
-
invalidatePreview(orderId);
|
|
1100
|
+
invalidatePreview(orderId, kind);
|
|
1061
1101
|
if (loadHistory) {
|
|
1062
1102
|
fetchMessages(result.conversation.id).then((history) => {
|
|
1063
1103
|
setMessages((prev) => {
|
|
@@ -1087,6 +1127,7 @@ function useConversation({
|
|
|
1087
1127
|
conversation,
|
|
1088
1128
|
createConversationWithMessage,
|
|
1089
1129
|
orderId,
|
|
1130
|
+
kind,
|
|
1090
1131
|
buildName,
|
|
1091
1132
|
participantIds,
|
|
1092
1133
|
joinChannel,
|
|
@@ -4496,6 +4537,7 @@ function CollabPanel({
|
|
|
4496
4537
|
orderId,
|
|
4497
4538
|
patientData,
|
|
4498
4539
|
participantIds,
|
|
4540
|
+
kind,
|
|
4499
4541
|
showSeenBy = true,
|
|
4500
4542
|
onBack,
|
|
4501
4543
|
hidePatientName = false,
|
|
@@ -4534,7 +4576,7 @@ function CollabPanel({
|
|
|
4534
4576
|
loadMoreMessages,
|
|
4535
4577
|
pinMessage,
|
|
4536
4578
|
unpinMessage
|
|
4537
|
-
} = useConversation({ orderId, patientData, participantIds });
|
|
4579
|
+
} = useConversation({ orderId, patientData, participantIds, kind });
|
|
4538
4580
|
React4.useEffect(() => {
|
|
4539
4581
|
onConversationChange?.(conversation);
|
|
4540
4582
|
}, [conversation, onConversationChange]);
|
|
@@ -4753,6 +4795,7 @@ function CollabPopup({
|
|
|
4753
4795
|
orderId,
|
|
4754
4796
|
patientData,
|
|
4755
4797
|
participantIds,
|
|
4798
|
+
kind,
|
|
4756
4799
|
isOpen,
|
|
4757
4800
|
onClose,
|
|
4758
4801
|
onBack,
|
|
@@ -4941,6 +4984,7 @@ function CollabPopup({
|
|
|
4941
4984
|
orderId,
|
|
4942
4985
|
patientData,
|
|
4943
4986
|
participantIds,
|
|
4987
|
+
kind,
|
|
4944
4988
|
hidePatientName: true,
|
|
4945
4989
|
onConversationChange: setLoadedConversation,
|
|
4946
4990
|
showSettings,
|
|
@@ -5058,7 +5102,8 @@ function useInlineCollab({
|
|
|
5058
5102
|
orderId,
|
|
5059
5103
|
patientData,
|
|
5060
5104
|
participantIds = [],
|
|
5061
|
-
messageLimit = 5
|
|
5105
|
+
messageLimit = 5,
|
|
5106
|
+
kind = "case"
|
|
5062
5107
|
}) {
|
|
5063
5108
|
const {
|
|
5064
5109
|
socket,
|
|
@@ -5077,6 +5122,8 @@ function useInlineCollab({
|
|
|
5077
5122
|
const elementRef = React4.useRef(null);
|
|
5078
5123
|
const observerRef = React4.useRef(null);
|
|
5079
5124
|
const subscribedConversationIdRef = React4.useRef(null);
|
|
5125
|
+
const latestMessageIdRef = React4.useRef(null);
|
|
5126
|
+
const lastMarkedReadIdRef = React4.useRef(null);
|
|
5080
5127
|
const channelSubscriptionRef = React4.useRef(null);
|
|
5081
5128
|
const trimToLimit = React4.useCallback(
|
|
5082
5129
|
(msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
|
|
@@ -5086,13 +5133,25 @@ function useInlineCollab({
|
|
|
5086
5133
|
() => buildChannelName(patientData),
|
|
5087
5134
|
[patientData]
|
|
5088
5135
|
);
|
|
5136
|
+
React4.useEffect(() => {
|
|
5137
|
+
latestMessageIdRef.current = messages[messages.length - 1]?.id ?? null;
|
|
5138
|
+
}, [messages]);
|
|
5139
|
+
const markLatestRead = React4.useCallback(
|
|
5140
|
+
(conversationId) => {
|
|
5141
|
+
const messageId = latestMessageIdRef.current;
|
|
5142
|
+
if (!messageId || lastMarkedReadIdRef.current === messageId) return;
|
|
5143
|
+
lastMarkedReadIdRef.current = messageId;
|
|
5144
|
+
socket.markAsRead(conversationId, messageId);
|
|
5145
|
+
},
|
|
5146
|
+
[socket]
|
|
5147
|
+
);
|
|
5089
5148
|
React4.useEffect(() => {
|
|
5090
5149
|
let cancelled = false;
|
|
5091
5150
|
const load = async () => {
|
|
5092
5151
|
setIsLoading(true);
|
|
5093
5152
|
setError(null);
|
|
5094
5153
|
try {
|
|
5095
|
-
const result = await requestPreview(orderId);
|
|
5154
|
+
const result = await requestPreview(orderId, kind);
|
|
5096
5155
|
if (cancelled) return;
|
|
5097
5156
|
setPreview(result);
|
|
5098
5157
|
if (result) {
|
|
@@ -5188,7 +5247,8 @@ function useInlineCollab({
|
|
|
5188
5247
|
try {
|
|
5189
5248
|
const result = await createConversationWithMessage(orderId, payload, {
|
|
5190
5249
|
name: buildName(),
|
|
5191
|
-
participantIds
|
|
5250
|
+
participantIds,
|
|
5251
|
+
kind
|
|
5192
5252
|
});
|
|
5193
5253
|
const newPreview = {
|
|
5194
5254
|
conversationId: result.conversation.id,
|
|
@@ -5202,7 +5262,7 @@ function useInlineCollab({
|
|
|
5202
5262
|
setPreview(newPreview);
|
|
5203
5263
|
setMessages([result.message]);
|
|
5204
5264
|
setParticipants(result.conversation.participants);
|
|
5205
|
-
invalidatePreview(orderId);
|
|
5265
|
+
invalidatePreview(orderId, kind);
|
|
5206
5266
|
if (elementRef.current) {
|
|
5207
5267
|
subscribe(result.conversation.id);
|
|
5208
5268
|
}
|
|
@@ -5219,6 +5279,7 @@ function useInlineCollab({
|
|
|
5219
5279
|
try {
|
|
5220
5280
|
await socket.sendMessage(preview.conversationId, payload);
|
|
5221
5281
|
setUnreadCount(0);
|
|
5282
|
+
markLatestRead(preview.conversationId);
|
|
5222
5283
|
} catch (err) {
|
|
5223
5284
|
setError(err instanceof Error ? err.message : "Failed to send message");
|
|
5224
5285
|
config.onError?.({
|
|
@@ -5237,7 +5298,8 @@ function useInlineCollab({
|
|
|
5237
5298
|
invalidatePreview,
|
|
5238
5299
|
subscribe,
|
|
5239
5300
|
socket,
|
|
5240
|
-
config
|
|
5301
|
+
config,
|
|
5302
|
+
markLatestRead
|
|
5241
5303
|
]
|
|
5242
5304
|
);
|
|
5243
5305
|
const sendMessage = React4.useCallback(
|
|
@@ -5325,6 +5387,7 @@ function CollabInline({
|
|
|
5325
5387
|
orderId,
|
|
5326
5388
|
patientData,
|
|
5327
5389
|
participantIds,
|
|
5390
|
+
kind,
|
|
5328
5391
|
onExpand,
|
|
5329
5392
|
messageLimit = 1,
|
|
5330
5393
|
placeholder = "Discuss this case\u2026",
|
|
@@ -5343,7 +5406,7 @@ function CollabInline({
|
|
|
5343
5406
|
sendMessage,
|
|
5344
5407
|
sendAudioMessage,
|
|
5345
5408
|
containerRef
|
|
5346
|
-
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
|
|
5409
|
+
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit, kind });
|
|
5347
5410
|
const containerStyle = {
|
|
5348
5411
|
...styles14.container,
|
|
5349
5412
|
backgroundColor: pal.bg,
|
|
@@ -5835,6 +5898,11 @@ function useConversationList(options) {
|
|
|
5835
5898
|
totalUnread
|
|
5836
5899
|
};
|
|
5837
5900
|
}
|
|
5901
|
+
|
|
5902
|
+
// src/core/types.ts
|
|
5903
|
+
function isHelpKind(kind) {
|
|
5904
|
+
return kind === "help_lab" || kind === "help_radiologist";
|
|
5905
|
+
}
|
|
5838
5906
|
function ConversationListItem({
|
|
5839
5907
|
item,
|
|
5840
5908
|
isSelected,
|
|
@@ -5871,6 +5939,10 @@ function ConversationListItem({
|
|
|
5871
5939
|
children: displayName
|
|
5872
5940
|
}
|
|
5873
5941
|
),
|
|
5942
|
+
isHelpKind(item.kind) && /* A case can show two rows with the same patient name — the shared
|
|
5943
|
+
case thread and a private help thread. Without this the two are
|
|
5944
|
+
indistinguishable in the inbox. */
|
|
5945
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles15.helpPill, "aria-label": "Help thread", children: "Help" }),
|
|
5874
5946
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5875
5947
|
"span",
|
|
5876
5948
|
{
|
|
@@ -6017,6 +6089,17 @@ var styles15 = {
|
|
|
6017
6089
|
nameUnread: {
|
|
6018
6090
|
fontWeight: FONT_WEIGHT.bold
|
|
6019
6091
|
},
|
|
6092
|
+
helpPill: {
|
|
6093
|
+
fontSize: FONT_SIZE.xs,
|
|
6094
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
6095
|
+
color: COLOR.primary,
|
|
6096
|
+
background: COLOR.primaryBg,
|
|
6097
|
+
borderRadius: RADIUS.sm,
|
|
6098
|
+
padding: `0 ${SPACE.S2}`,
|
|
6099
|
+
lineHeight: "16px",
|
|
6100
|
+
flexShrink: 0,
|
|
6101
|
+
whiteSpace: "nowrap"
|
|
6102
|
+
},
|
|
6020
6103
|
time: {
|
|
6021
6104
|
fontSize: FONT_SIZE.xs,
|
|
6022
6105
|
color: COLOR.neutral500,
|
|
@@ -6791,6 +6874,7 @@ exports.THEME_DEFAULTS = THEME_DEFAULTS;
|
|
|
6791
6874
|
exports.THEME_VAR = THEME_VAR;
|
|
6792
6875
|
exports.TYPING_DEBOUNCE_MS = TYPING_DEBOUNCE_MS;
|
|
6793
6876
|
exports.applyThemeOverrides = applyThemeOverrides;
|
|
6877
|
+
exports.isHelpKind = isHelpKind;
|
|
6794
6878
|
exports.useAudioRecorder = useAudioRecorder;
|
|
6795
6879
|
exports.useChannelSettings = useChannelSettings;
|
|
6796
6880
|
exports.useCollab = useCollab;
|