@natoe/colab 0.1.27 → 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.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 pendingOrderIds = useRef(/* @__PURE__ */ new Set());
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 orderIds = Array.from(pendingOrderIds.current);
635
+ const keys = Array.from(pendingKeys.current);
626
636
  const resolvers = new Map(pendingResolvers.current);
627
- pendingOrderIds.current.clear();
637
+ pendingKeys.current.clear();
628
638
  pendingResolvers.current.clear();
629
639
  batchScheduled.current = false;
630
- if (orderIds.length === 0) return;
631
- try {
632
- const query = orderIds.map((id) => `order_ids[]=${encodeURIComponent(id)}`).join("&");
633
- const response = await fetch(`${apiBaseUrl}/api/natoe-colab/conversations/previews?${query}`, {
634
- headers: authHeaders()
635
- });
636
- if (!response.ok) throw new Error(`Preview batch failed: ${response.status}`);
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
- if (previewCache.current.has(orderId)) {
659
- return Promise.resolve(previewCache.current.get(orderId) ?? null);
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
- pendingOrderIds.current.add(orderId);
663
- const existing = pendingResolvers.current.get(orderId) || [];
686
+ pendingKeys.current.add(key);
687
+ const existing = pendingResolvers.current.get(key) || [];
664
688
  existing.push(resolve);
665
- pendingResolvers.current.set(orderId, existing);
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
- previewCache.current.delete(orderId);
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,
@@ -5100,7 +5145,7 @@ function useInlineCollab({
5100
5145
  setIsLoading(true);
5101
5146
  setError(null);
5102
5147
  try {
5103
- const result = await requestPreview(orderId);
5148
+ const result = await requestPreview(orderId, kind);
5104
5149
  if (cancelled) return;
5105
5150
  setPreview(result);
5106
5151
  if (result) {
@@ -5196,7 +5241,8 @@ function useInlineCollab({
5196
5241
  try {
5197
5242
  const result = await createConversationWithMessage(orderId, payload, {
5198
5243
  name: buildName(),
5199
- participantIds
5244
+ participantIds,
5245
+ kind
5200
5246
  });
5201
5247
  const newPreview = {
5202
5248
  conversationId: result.conversation.id,
@@ -5210,7 +5256,7 @@ function useInlineCollab({
5210
5256
  setPreview(newPreview);
5211
5257
  setMessages([result.message]);
5212
5258
  setParticipants(result.conversation.participants);
5213
- invalidatePreview(orderId);
5259
+ invalidatePreview(orderId, kind);
5214
5260
  if (elementRef.current) {
5215
5261
  subscribe(result.conversation.id);
5216
5262
  }
@@ -5335,6 +5381,7 @@ function CollabInline({
5335
5381
  orderId,
5336
5382
  patientData,
5337
5383
  participantIds,
5384
+ kind,
5338
5385
  onExpand,
5339
5386
  messageLimit = 1,
5340
5387
  placeholder = "Discuss this case\u2026",
@@ -5353,7 +5400,7 @@ function CollabInline({
5353
5400
  sendMessage,
5354
5401
  sendAudioMessage,
5355
5402
  containerRef
5356
- } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
5403
+ } = useInlineCollab({ orderId, patientData, participantIds, messageLimit, kind });
5357
5404
  const containerStyle = {
5358
5405
  ...styles14.container,
5359
5406
  backgroundColor: pal.bg,
@@ -5845,6 +5892,11 @@ function useConversationList(options) {
5845
5892
  totalUnread
5846
5893
  };
5847
5894
  }
5895
+
5896
+ // src/core/types.ts
5897
+ function isHelpKind(kind) {
5898
+ return kind === "help_lab" || kind === "help_radiologist";
5899
+ }
5848
5900
  function ConversationListItem({
5849
5901
  item,
5850
5902
  isSelected,
@@ -5881,6 +5933,10 @@ function ConversationListItem({
5881
5933
  children: displayName
5882
5934
  }
5883
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" }),
5884
5940
  /* @__PURE__ */ jsx(
5885
5941
  "span",
5886
5942
  {
@@ -6027,6 +6083,17 @@ var styles15 = {
6027
6083
  nameUnread: {
6028
6084
  fontWeight: FONT_WEIGHT.bold
6029
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
+ },
6030
6097
  time: {
6031
6098
  fontSize: FONT_SIZE.xs,
6032
6099
  color: COLOR.neutral500,
@@ -6770,6 +6837,6 @@ function usePinnedMessages({
6770
6837
  };
6771
6838
  }
6772
6839
 
6773
- 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 };
6774
6841
  //# sourceMappingURL=index.mjs.map
6775
6842
  //# sourceMappingURL=index.mjs.map