@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.mjs
CHANGED
|
@@ -576,6 +576,16 @@ function applyThemeOverrides(theme) {
|
|
|
576
576
|
}
|
|
577
577
|
});
|
|
578
578
|
}
|
|
579
|
+
function previewKey(orderId, kind) {
|
|
580
|
+
return `${kind}|${orderId}`;
|
|
581
|
+
}
|
|
582
|
+
function parsePreviewKey(key) {
|
|
583
|
+
const separator = key.indexOf("|");
|
|
584
|
+
return {
|
|
585
|
+
kind: key.slice(0, separator),
|
|
586
|
+
orderId: key.slice(separator + 1)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
579
589
|
var CollabContext = createContext(null);
|
|
580
590
|
function useCollab() {
|
|
581
591
|
const context = useContext(CollabContext);
|
|
@@ -591,7 +601,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
591
601
|
});
|
|
592
602
|
const [unreadCounts, setUnreadCounts] = useState({});
|
|
593
603
|
const [unreadCountsByOrder, setUnreadCountsByOrder] = useState({});
|
|
594
|
-
const
|
|
604
|
+
const pendingKeys = useRef(/* @__PURE__ */ new Set());
|
|
595
605
|
const pendingResolvers = useRef(/* @__PURE__ */ new Map());
|
|
596
606
|
const previewCache = useRef(/* @__PURE__ */ new Map());
|
|
597
607
|
const batchScheduled = useRef(false);
|
|
@@ -622,47 +632,61 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
622
632
|
[config]
|
|
623
633
|
);
|
|
624
634
|
const flushPreviewBatch = useCallback(async () => {
|
|
625
|
-
const
|
|
635
|
+
const keys = Array.from(pendingKeys.current);
|
|
626
636
|
const resolvers = new Map(pendingResolvers.current);
|
|
627
|
-
|
|
637
|
+
pendingKeys.current.clear();
|
|
628
638
|
pendingResolvers.current.clear();
|
|
629
639
|
batchScheduled.current = false;
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const data = snakeToCamel(await response.json());
|
|
638
|
-
orderIds.forEach((orderId) => {
|
|
639
|
-
const preview = data.previews[orderId] ?? null;
|
|
640
|
-
if (preview !== null) {
|
|
641
|
-
previewCache.current.set(orderId, preview);
|
|
642
|
-
}
|
|
643
|
-
resolvers.get(orderId)?.forEach((resolve) => resolve(preview));
|
|
644
|
-
});
|
|
645
|
-
} catch (error) {
|
|
646
|
-
config.onError?.({
|
|
647
|
-
code: "PREVIEW_BATCH_ERROR",
|
|
648
|
-
message: "Failed to fetch conversation previews",
|
|
649
|
-
details: error
|
|
650
|
-
});
|
|
651
|
-
orderIds.forEach((orderId) => {
|
|
652
|
-
resolvers.get(orderId)?.forEach((resolve) => resolve(null));
|
|
653
|
-
});
|
|
640
|
+
if (keys.length === 0) return;
|
|
641
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const key of keys) {
|
|
643
|
+
const { orderId, kind } = parsePreviewKey(key);
|
|
644
|
+
const list = byKind.get(kind) ?? [];
|
|
645
|
+
list.push(orderId);
|
|
646
|
+
byKind.set(kind, list);
|
|
654
647
|
}
|
|
648
|
+
await Promise.all(
|
|
649
|
+
Array.from(byKind.entries()).map(async ([kind, orderIds]) => {
|
|
650
|
+
try {
|
|
651
|
+
const query = orderIds.map((id) => `order_ids[]=${encodeURIComponent(id)}`).join("&");
|
|
652
|
+
const response = await fetch(
|
|
653
|
+
`${apiBaseUrl}/api/natoe-colab/conversations/previews?${query}&kind=${encodeURIComponent(kind)}`,
|
|
654
|
+
{ headers: authHeaders() }
|
|
655
|
+
);
|
|
656
|
+
if (!response.ok) throw new Error(`Preview batch failed: ${response.status}`);
|
|
657
|
+
const data = snakeToCamel(await response.json());
|
|
658
|
+
orderIds.forEach((orderId) => {
|
|
659
|
+
const key = previewKey(orderId, kind);
|
|
660
|
+
const preview = data.previews[orderId] ?? null;
|
|
661
|
+
if (preview !== null) {
|
|
662
|
+
previewCache.current.set(key, preview);
|
|
663
|
+
}
|
|
664
|
+
resolvers.get(key)?.forEach((resolve) => resolve(preview));
|
|
665
|
+
});
|
|
666
|
+
} catch (error) {
|
|
667
|
+
config.onError?.({
|
|
668
|
+
code: "PREVIEW_BATCH_ERROR",
|
|
669
|
+
message: "Failed to fetch conversation previews",
|
|
670
|
+
details: error
|
|
671
|
+
});
|
|
672
|
+
orderIds.forEach((orderId) => {
|
|
673
|
+
resolvers.get(previewKey(orderId, kind))?.forEach((resolve) => resolve(null));
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
})
|
|
677
|
+
);
|
|
655
678
|
}, [apiBaseUrl, authHeaders, config]);
|
|
656
679
|
const requestPreview = useCallback(
|
|
657
|
-
(orderId) => {
|
|
658
|
-
|
|
659
|
-
|
|
680
|
+
(orderId, kind = "case") => {
|
|
681
|
+
const key = previewKey(orderId, kind);
|
|
682
|
+
if (previewCache.current.has(key)) {
|
|
683
|
+
return Promise.resolve(previewCache.current.get(key) ?? null);
|
|
660
684
|
}
|
|
661
685
|
return new Promise((resolve) => {
|
|
662
|
-
|
|
663
|
-
const existing = pendingResolvers.current.get(
|
|
686
|
+
pendingKeys.current.add(key);
|
|
687
|
+
const existing = pendingResolvers.current.get(key) || [];
|
|
664
688
|
existing.push(resolve);
|
|
665
|
-
pendingResolvers.current.set(
|
|
689
|
+
pendingResolvers.current.set(key, existing);
|
|
666
690
|
if (!batchScheduled.current) {
|
|
667
691
|
batchScheduled.current = true;
|
|
668
692
|
queueMicrotask(flushPreviewBatch);
|
|
@@ -671,8 +695,16 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
671
695
|
},
|
|
672
696
|
[flushPreviewBatch]
|
|
673
697
|
);
|
|
674
|
-
const invalidatePreview = useCallback((orderId) => {
|
|
675
|
-
|
|
698
|
+
const invalidatePreview = useCallback((orderId, kind) => {
|
|
699
|
+
if (kind) {
|
|
700
|
+
previewCache.current.delete(previewKey(orderId, kind));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
for (const key of Array.from(previewCache.current.keys())) {
|
|
704
|
+
if (parsePreviewKey(key).orderId === orderId) {
|
|
705
|
+
previewCache.current.delete(key);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
676
708
|
}, []);
|
|
677
709
|
const fetchMessages = useCallback(
|
|
678
710
|
async (conversationId, options = {}) => {
|
|
@@ -699,11 +731,17 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
699
731
|
message: "Cannot create conversation: orderId is empty. Host app passed an order with no orderId/mainOrderId set."
|
|
700
732
|
};
|
|
701
733
|
}
|
|
734
|
+
const kind = options.kind ?? "case";
|
|
702
735
|
const response = await fetch(`${apiBaseUrl}/api/natoe-colab/orders/${orderId}/messages`, {
|
|
703
736
|
method: "POST",
|
|
704
737
|
headers: authHeaders(),
|
|
705
738
|
body: JSON.stringify({
|
|
706
739
|
message,
|
|
740
|
+
// Top-level, matching the backend's `params["kind"]`. It selects
|
|
741
|
+
// BOTH which thread is addressed and which authorization path runs
|
|
742
|
+
// — help kinds go through Access.can_access_help?/5, not the
|
|
743
|
+
// broader case-chat check.
|
|
744
|
+
kind,
|
|
707
745
|
conversation: {
|
|
708
746
|
name: options.name,
|
|
709
747
|
participant_ids: options.participantIds ?? []
|
|
@@ -715,7 +753,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
715
753
|
throw { code: "CREATE_ERROR", message: "Failed to create conversation", details: error };
|
|
716
754
|
}
|
|
717
755
|
const data = snakeToCamel(await response.json());
|
|
718
|
-
previewCache.current.delete(orderId);
|
|
756
|
+
previewCache.current.delete(previewKey(orderId, kind));
|
|
719
757
|
return data;
|
|
720
758
|
},
|
|
721
759
|
[apiBaseUrl, authHeaders]
|
|
@@ -849,7 +887,8 @@ function useConversation({
|
|
|
849
887
|
orderId,
|
|
850
888
|
patientData,
|
|
851
889
|
participantIds = [],
|
|
852
|
-
loadHistory = true
|
|
890
|
+
loadHistory = true,
|
|
891
|
+
kind = "case"
|
|
853
892
|
}) {
|
|
854
893
|
const {
|
|
855
894
|
socket,
|
|
@@ -977,7 +1016,7 @@ function useConversation({
|
|
|
977
1016
|
setIsLoading(true);
|
|
978
1017
|
setError(null);
|
|
979
1018
|
try {
|
|
980
|
-
const preview = await requestPreview(orderId);
|
|
1019
|
+
const preview = await requestPreview(orderId, kind);
|
|
981
1020
|
if (cancelled) return;
|
|
982
1021
|
if (!preview) {
|
|
983
1022
|
setConversation(null);
|
|
@@ -1034,7 +1073,7 @@ function useConversation({
|
|
|
1034
1073
|
markedReadIdsRef.current.clear();
|
|
1035
1074
|
setIsConnected(false);
|
|
1036
1075
|
};
|
|
1037
|
-
}, [orderId]);
|
|
1076
|
+
}, [orderId, kind]);
|
|
1038
1077
|
const ensureConversation = useCallback(
|
|
1039
1078
|
async (payload) => {
|
|
1040
1079
|
if (conversation) return { conv: conversation, persistedByCreate: false };
|
|
@@ -1045,13 +1084,14 @@ function useConversation({
|
|
|
1045
1084
|
const inflight = (async () => {
|
|
1046
1085
|
const result = await createConversationWithMessage(orderId, payload, {
|
|
1047
1086
|
name: buildName(),
|
|
1048
|
-
participantIds
|
|
1087
|
+
participantIds,
|
|
1088
|
+
kind
|
|
1049
1089
|
});
|
|
1050
1090
|
setConversation(result.conversation);
|
|
1051
1091
|
setParticipants(result.conversation.participants);
|
|
1052
1092
|
setMessages([result.message]);
|
|
1053
1093
|
joinChannel(result.conversation);
|
|
1054
|
-
invalidatePreview(orderId);
|
|
1094
|
+
invalidatePreview(orderId, kind);
|
|
1055
1095
|
if (loadHistory) {
|
|
1056
1096
|
fetchMessages(result.conversation.id).then((history) => {
|
|
1057
1097
|
setMessages((prev) => {
|
|
@@ -1081,6 +1121,7 @@ function useConversation({
|
|
|
1081
1121
|
conversation,
|
|
1082
1122
|
createConversationWithMessage,
|
|
1083
1123
|
orderId,
|
|
1124
|
+
kind,
|
|
1084
1125
|
buildName,
|
|
1085
1126
|
participantIds,
|
|
1086
1127
|
joinChannel,
|
|
@@ -4490,6 +4531,7 @@ function CollabPanel({
|
|
|
4490
4531
|
orderId,
|
|
4491
4532
|
patientData,
|
|
4492
4533
|
participantIds,
|
|
4534
|
+
kind,
|
|
4493
4535
|
showSeenBy = true,
|
|
4494
4536
|
onBack,
|
|
4495
4537
|
hidePatientName = false,
|
|
@@ -4528,7 +4570,7 @@ function CollabPanel({
|
|
|
4528
4570
|
loadMoreMessages,
|
|
4529
4571
|
pinMessage,
|
|
4530
4572
|
unpinMessage
|
|
4531
|
-
} = useConversation({ orderId, patientData, participantIds });
|
|
4573
|
+
} = useConversation({ orderId, patientData, participantIds, kind });
|
|
4532
4574
|
useEffect(() => {
|
|
4533
4575
|
onConversationChange?.(conversation);
|
|
4534
4576
|
}, [conversation, onConversationChange]);
|
|
@@ -4747,6 +4789,7 @@ function CollabPopup({
|
|
|
4747
4789
|
orderId,
|
|
4748
4790
|
patientData,
|
|
4749
4791
|
participantIds,
|
|
4792
|
+
kind,
|
|
4750
4793
|
isOpen,
|
|
4751
4794
|
onClose,
|
|
4752
4795
|
onBack,
|
|
@@ -4935,6 +4978,7 @@ function CollabPopup({
|
|
|
4935
4978
|
orderId,
|
|
4936
4979
|
patientData,
|
|
4937
4980
|
participantIds,
|
|
4981
|
+
kind,
|
|
4938
4982
|
hidePatientName: true,
|
|
4939
4983
|
onConversationChange: setLoadedConversation,
|
|
4940
4984
|
showSettings,
|
|
@@ -5052,7 +5096,8 @@ function useInlineCollab({
|
|
|
5052
5096
|
orderId,
|
|
5053
5097
|
patientData,
|
|
5054
5098
|
participantIds = [],
|
|
5055
|
-
messageLimit = 5
|
|
5099
|
+
messageLimit = 5,
|
|
5100
|
+
kind = "case"
|
|
5056
5101
|
}) {
|
|
5057
5102
|
const {
|
|
5058
5103
|
socket,
|
|
@@ -5071,6 +5116,8 @@ function useInlineCollab({
|
|
|
5071
5116
|
const elementRef = useRef(null);
|
|
5072
5117
|
const observerRef = useRef(null);
|
|
5073
5118
|
const subscribedConversationIdRef = useRef(null);
|
|
5119
|
+
const latestMessageIdRef = useRef(null);
|
|
5120
|
+
const lastMarkedReadIdRef = useRef(null);
|
|
5074
5121
|
const channelSubscriptionRef = useRef(null);
|
|
5075
5122
|
const trimToLimit = useCallback(
|
|
5076
5123
|
(msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
|
|
@@ -5080,13 +5127,25 @@ function useInlineCollab({
|
|
|
5080
5127
|
() => buildChannelName(patientData),
|
|
5081
5128
|
[patientData]
|
|
5082
5129
|
);
|
|
5130
|
+
useEffect(() => {
|
|
5131
|
+
latestMessageIdRef.current = messages[messages.length - 1]?.id ?? null;
|
|
5132
|
+
}, [messages]);
|
|
5133
|
+
const markLatestRead = useCallback(
|
|
5134
|
+
(conversationId) => {
|
|
5135
|
+
const messageId = latestMessageIdRef.current;
|
|
5136
|
+
if (!messageId || lastMarkedReadIdRef.current === messageId) return;
|
|
5137
|
+
lastMarkedReadIdRef.current = messageId;
|
|
5138
|
+
socket.markAsRead(conversationId, messageId);
|
|
5139
|
+
},
|
|
5140
|
+
[socket]
|
|
5141
|
+
);
|
|
5083
5142
|
useEffect(() => {
|
|
5084
5143
|
let cancelled = false;
|
|
5085
5144
|
const load = async () => {
|
|
5086
5145
|
setIsLoading(true);
|
|
5087
5146
|
setError(null);
|
|
5088
5147
|
try {
|
|
5089
|
-
const result = await requestPreview(orderId);
|
|
5148
|
+
const result = await requestPreview(orderId, kind);
|
|
5090
5149
|
if (cancelled) return;
|
|
5091
5150
|
setPreview(result);
|
|
5092
5151
|
if (result) {
|
|
@@ -5182,7 +5241,8 @@ function useInlineCollab({
|
|
|
5182
5241
|
try {
|
|
5183
5242
|
const result = await createConversationWithMessage(orderId, payload, {
|
|
5184
5243
|
name: buildName(),
|
|
5185
|
-
participantIds
|
|
5244
|
+
participantIds,
|
|
5245
|
+
kind
|
|
5186
5246
|
});
|
|
5187
5247
|
const newPreview = {
|
|
5188
5248
|
conversationId: result.conversation.id,
|
|
@@ -5196,7 +5256,7 @@ function useInlineCollab({
|
|
|
5196
5256
|
setPreview(newPreview);
|
|
5197
5257
|
setMessages([result.message]);
|
|
5198
5258
|
setParticipants(result.conversation.participants);
|
|
5199
|
-
invalidatePreview(orderId);
|
|
5259
|
+
invalidatePreview(orderId, kind);
|
|
5200
5260
|
if (elementRef.current) {
|
|
5201
5261
|
subscribe(result.conversation.id);
|
|
5202
5262
|
}
|
|
@@ -5213,6 +5273,7 @@ function useInlineCollab({
|
|
|
5213
5273
|
try {
|
|
5214
5274
|
await socket.sendMessage(preview.conversationId, payload);
|
|
5215
5275
|
setUnreadCount(0);
|
|
5276
|
+
markLatestRead(preview.conversationId);
|
|
5216
5277
|
} catch (err) {
|
|
5217
5278
|
setError(err instanceof Error ? err.message : "Failed to send message");
|
|
5218
5279
|
config.onError?.({
|
|
@@ -5231,7 +5292,8 @@ function useInlineCollab({
|
|
|
5231
5292
|
invalidatePreview,
|
|
5232
5293
|
subscribe,
|
|
5233
5294
|
socket,
|
|
5234
|
-
config
|
|
5295
|
+
config,
|
|
5296
|
+
markLatestRead
|
|
5235
5297
|
]
|
|
5236
5298
|
);
|
|
5237
5299
|
const sendMessage = useCallback(
|
|
@@ -5319,6 +5381,7 @@ function CollabInline({
|
|
|
5319
5381
|
orderId,
|
|
5320
5382
|
patientData,
|
|
5321
5383
|
participantIds,
|
|
5384
|
+
kind,
|
|
5322
5385
|
onExpand,
|
|
5323
5386
|
messageLimit = 1,
|
|
5324
5387
|
placeholder = "Discuss this case\u2026",
|
|
@@ -5337,7 +5400,7 @@ function CollabInline({
|
|
|
5337
5400
|
sendMessage,
|
|
5338
5401
|
sendAudioMessage,
|
|
5339
5402
|
containerRef
|
|
5340
|
-
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
|
|
5403
|
+
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit, kind });
|
|
5341
5404
|
const containerStyle = {
|
|
5342
5405
|
...styles14.container,
|
|
5343
5406
|
backgroundColor: pal.bg,
|
|
@@ -5829,6 +5892,11 @@ function useConversationList(options) {
|
|
|
5829
5892
|
totalUnread
|
|
5830
5893
|
};
|
|
5831
5894
|
}
|
|
5895
|
+
|
|
5896
|
+
// src/core/types.ts
|
|
5897
|
+
function isHelpKind(kind) {
|
|
5898
|
+
return kind === "help_lab" || kind === "help_radiologist";
|
|
5899
|
+
}
|
|
5832
5900
|
function ConversationListItem({
|
|
5833
5901
|
item,
|
|
5834
5902
|
isSelected,
|
|
@@ -5865,6 +5933,10 @@ function ConversationListItem({
|
|
|
5865
5933
|
children: displayName
|
|
5866
5934
|
}
|
|
5867
5935
|
),
|
|
5936
|
+
isHelpKind(item.kind) && /* A case can show two rows with the same patient name — the shared
|
|
5937
|
+
case thread and a private help thread. Without this the two are
|
|
5938
|
+
indistinguishable in the inbox. */
|
|
5939
|
+
/* @__PURE__ */ jsx("span", { style: styles15.helpPill, "aria-label": "Help thread", children: "Help" }),
|
|
5868
5940
|
/* @__PURE__ */ jsx(
|
|
5869
5941
|
"span",
|
|
5870
5942
|
{
|
|
@@ -6011,6 +6083,17 @@ var styles15 = {
|
|
|
6011
6083
|
nameUnread: {
|
|
6012
6084
|
fontWeight: FONT_WEIGHT.bold
|
|
6013
6085
|
},
|
|
6086
|
+
helpPill: {
|
|
6087
|
+
fontSize: FONT_SIZE.xs,
|
|
6088
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
6089
|
+
color: COLOR.primary,
|
|
6090
|
+
background: COLOR.primaryBg,
|
|
6091
|
+
borderRadius: RADIUS.sm,
|
|
6092
|
+
padding: `0 ${SPACE.S2}`,
|
|
6093
|
+
lineHeight: "16px",
|
|
6094
|
+
flexShrink: 0,
|
|
6095
|
+
whiteSpace: "nowrap"
|
|
6096
|
+
},
|
|
6014
6097
|
time: {
|
|
6015
6098
|
fontSize: FONT_SIZE.xs,
|
|
6016
6099
|
color: COLOR.neutral500,
|
|
@@ -6754,6 +6837,6 @@ function usePinnedMessages({
|
|
|
6754
6837
|
};
|
|
6755
6838
|
}
|
|
6756
6839
|
|
|
6757
|
-
export { AUDIO_MIME_TYPE, ChannelSettings, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, ConversationList, ConversationListItem, DEEP_LINK_PREFIX, EVENTS, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, MessageActionsMenu, MessageBubble, MessageInput, MessageList, ParticipantsList, PatientHeader, PinnedMessagesBar, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, applyThemeOverrides, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
|
|
6840
|
+
export { AUDIO_MIME_TYPE, ChannelSettings, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, ConversationList, ConversationListItem, DEEP_LINK_PREFIX, EVENTS, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, MessageActionsMenu, MessageBubble, MessageInput, MessageList, ParticipantsList, PatientHeader, PinnedMessagesBar, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, applyThemeOverrides, isHelpKind, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
|
|
6758
6841
|
//# sourceMappingURL=index.mjs.map
|
|
6759
6842
|
//# sourceMappingURL=index.mjs.map
|