@natoe/colab 0.1.0 → 0.1.2
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 +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +401 -530
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -363
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React4 = require('react');
|
|
4
4
|
var phoenix = require('phoenix');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var React4__default = /*#__PURE__*/_interopDefault(React4);
|
|
10
|
+
|
|
7
11
|
// src/components/CollabPanel.tsx
|
|
8
12
|
|
|
9
13
|
// src/core/constants.ts
|
|
@@ -316,25 +320,25 @@ var CollabSocket = class {
|
|
|
316
320
|
this.config = null;
|
|
317
321
|
}
|
|
318
322
|
};
|
|
319
|
-
var CollabContext =
|
|
323
|
+
var CollabContext = React4.createContext(null);
|
|
320
324
|
function useCollab() {
|
|
321
|
-
const context =
|
|
325
|
+
const context = React4.useContext(CollabContext);
|
|
322
326
|
if (!context) {
|
|
323
327
|
throw new Error("useCollab must be used within a <CollabProvider>");
|
|
324
328
|
}
|
|
325
329
|
return context;
|
|
326
330
|
}
|
|
327
331
|
function CollabProvider({ config, apiBaseUrl, children }) {
|
|
328
|
-
const [socket, setSocket] =
|
|
332
|
+
const [socket, setSocket] = React4.useState(() => {
|
|
329
333
|
if (typeof window === "undefined") return null;
|
|
330
334
|
return new CollabSocket();
|
|
331
335
|
});
|
|
332
|
-
const [unreadCounts, setUnreadCounts] =
|
|
333
|
-
const pendingOrderIds =
|
|
334
|
-
const pendingResolvers =
|
|
335
|
-
const previewCache =
|
|
336
|
-
const batchScheduled =
|
|
337
|
-
|
|
336
|
+
const [unreadCounts, setUnreadCounts] = React4.useState({});
|
|
337
|
+
const pendingOrderIds = React4.useRef(/* @__PURE__ */ new Set());
|
|
338
|
+
const pendingResolvers = React4.useRef(/* @__PURE__ */ new Map());
|
|
339
|
+
const previewCache = React4.useRef(/* @__PURE__ */ new Map());
|
|
340
|
+
const batchScheduled = React4.useRef(false);
|
|
341
|
+
React4.useEffect(() => {
|
|
338
342
|
if (typeof window === "undefined") return;
|
|
339
343
|
let s = socket;
|
|
340
344
|
if (!s) {
|
|
@@ -349,14 +353,14 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
349
353
|
s.connect(config);
|
|
350
354
|
}
|
|
351
355
|
}, [config.socketUrl, config.userId]);
|
|
352
|
-
const authHeaders =
|
|
356
|
+
const authHeaders = React4.useCallback(
|
|
353
357
|
() => ({
|
|
354
358
|
Authorization: `Bearer ${config.getToken()}`,
|
|
355
359
|
"Content-Type": "application/json"
|
|
356
360
|
}),
|
|
357
361
|
[config]
|
|
358
362
|
);
|
|
359
|
-
const flushPreviewBatch =
|
|
363
|
+
const flushPreviewBatch = React4.useCallback(async () => {
|
|
360
364
|
const orderIds = Array.from(pendingOrderIds.current);
|
|
361
365
|
const resolvers = new Map(pendingResolvers.current);
|
|
362
366
|
pendingOrderIds.current.clear();
|
|
@@ -386,7 +390,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
386
390
|
});
|
|
387
391
|
}
|
|
388
392
|
}, [apiBaseUrl, authHeaders, config]);
|
|
389
|
-
const requestPreview =
|
|
393
|
+
const requestPreview = React4.useCallback(
|
|
390
394
|
(orderId) => {
|
|
391
395
|
if (previewCache.current.has(orderId)) {
|
|
392
396
|
return Promise.resolve(previewCache.current.get(orderId) ?? null);
|
|
@@ -404,10 +408,10 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
404
408
|
},
|
|
405
409
|
[flushPreviewBatch]
|
|
406
410
|
);
|
|
407
|
-
const invalidatePreview =
|
|
411
|
+
const invalidatePreview = React4.useCallback((orderId) => {
|
|
408
412
|
previewCache.current.delete(orderId);
|
|
409
413
|
}, []);
|
|
410
|
-
const fetchMessages =
|
|
414
|
+
const fetchMessages = React4.useCallback(
|
|
411
415
|
async (conversationId, options = {}) => {
|
|
412
416
|
const limit = options.limit ?? MESSAGES_PAGE_SIZE;
|
|
413
417
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
@@ -423,7 +427,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
423
427
|
},
|
|
424
428
|
[apiBaseUrl, authHeaders]
|
|
425
429
|
);
|
|
426
|
-
const createConversationWithMessage =
|
|
430
|
+
const createConversationWithMessage = React4.useCallback(
|
|
427
431
|
async (orderId, message, options) => {
|
|
428
432
|
if (!orderId || !orderId.trim()) {
|
|
429
433
|
throw {
|
|
@@ -452,7 +456,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
452
456
|
},
|
|
453
457
|
[apiBaseUrl, authHeaders]
|
|
454
458
|
);
|
|
455
|
-
const addParticipant =
|
|
459
|
+
const addParticipant = React4.useCallback(
|
|
456
460
|
async (conversationId, userId) => {
|
|
457
461
|
const response = await fetch(
|
|
458
462
|
`${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/participants`,
|
|
@@ -468,7 +472,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
468
472
|
},
|
|
469
473
|
[apiBaseUrl, authHeaders]
|
|
470
474
|
);
|
|
471
|
-
const removeParticipant =
|
|
475
|
+
const removeParticipant = React4.useCallback(
|
|
472
476
|
async (conversationId, userId) => {
|
|
473
477
|
const response = await fetch(
|
|
474
478
|
`${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/participants/${userId}`,
|
|
@@ -483,7 +487,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
483
487
|
},
|
|
484
488
|
[apiBaseUrl, authHeaders]
|
|
485
489
|
);
|
|
486
|
-
const fetchConversationList =
|
|
490
|
+
const fetchConversationList = React4.useCallback(async () => {
|
|
487
491
|
const response = await fetch(`${apiBaseUrl}/api/natoe-colab/conversations`, {
|
|
488
492
|
headers: authHeaders()
|
|
489
493
|
});
|
|
@@ -492,7 +496,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
492
496
|
}
|
|
493
497
|
return snakeToCamel(await response.json());
|
|
494
498
|
}, [apiBaseUrl, authHeaders]);
|
|
495
|
-
const pinMessage =
|
|
499
|
+
const pinMessage = React4.useCallback(
|
|
496
500
|
async (conversationId, messageId) => {
|
|
497
501
|
const response = await fetch(
|
|
498
502
|
`${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/messages/${messageId}/pin`,
|
|
@@ -508,7 +512,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
508
512
|
},
|
|
509
513
|
[apiBaseUrl, authHeaders]
|
|
510
514
|
);
|
|
511
|
-
const unpinMessage =
|
|
515
|
+
const unpinMessage = React4.useCallback(
|
|
512
516
|
async (conversationId, messageId) => {
|
|
513
517
|
const response = await fetch(
|
|
514
518
|
`${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/messages/${messageId}/pin`,
|
|
@@ -523,7 +527,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
523
527
|
},
|
|
524
528
|
[apiBaseUrl, authHeaders]
|
|
525
529
|
);
|
|
526
|
-
const fetchPinnedMessages =
|
|
530
|
+
const fetchPinnedMessages = React4.useCallback(
|
|
527
531
|
async (conversationId) => {
|
|
528
532
|
const response = await fetch(
|
|
529
533
|
`${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/pinned`,
|
|
@@ -587,25 +591,25 @@ function useConversation({
|
|
|
587
591
|
pinMessage: pinMessageApi,
|
|
588
592
|
unpinMessage: unpinMessageApi
|
|
589
593
|
} = useCollab();
|
|
590
|
-
const [conversation, setConversation] =
|
|
591
|
-
const [messages, setMessages] =
|
|
592
|
-
const [pinnedMessages, setPinnedMessages] =
|
|
593
|
-
const [participants, setParticipants] =
|
|
594
|
-
const [typingUsers, setTypingUsers] =
|
|
595
|
-
const [isLoading, setIsLoading] =
|
|
596
|
-
const [hasMore, setHasMore] =
|
|
597
|
-
const [error, setError] =
|
|
598
|
-
const [isConnected, setIsConnected] =
|
|
599
|
-
const [replyTo, setReplyTo] =
|
|
600
|
-
const joinedConversationId =
|
|
601
|
-
const typingTimers =
|
|
602
|
-
const ensureConversationInFlight =
|
|
603
|
-
const markedReadIdsRef =
|
|
604
|
-
const buildName =
|
|
594
|
+
const [conversation, setConversation] = React4.useState(null);
|
|
595
|
+
const [messages, setMessages] = React4.useState([]);
|
|
596
|
+
const [pinnedMessages, setPinnedMessages] = React4.useState([]);
|
|
597
|
+
const [participants, setParticipants] = React4.useState([]);
|
|
598
|
+
const [typingUsers, setTypingUsers] = React4.useState([]);
|
|
599
|
+
const [isLoading, setIsLoading] = React4.useState(true);
|
|
600
|
+
const [hasMore, setHasMore] = React4.useState(true);
|
|
601
|
+
const [error, setError] = React4.useState(null);
|
|
602
|
+
const [isConnected, setIsConnected] = React4.useState(false);
|
|
603
|
+
const [replyTo, setReplyTo] = React4.useState(null);
|
|
604
|
+
const joinedConversationId = React4.useRef(null);
|
|
605
|
+
const typingTimers = React4.useRef(/* @__PURE__ */ new Map());
|
|
606
|
+
const ensureConversationInFlight = React4.useRef(null);
|
|
607
|
+
const markedReadIdsRef = React4.useRef(/* @__PURE__ */ new Set());
|
|
608
|
+
const buildName = React4.useCallback(
|
|
605
609
|
() => buildChannelName(patientData),
|
|
606
610
|
[patientData]
|
|
607
611
|
);
|
|
608
|
-
const joinChannel =
|
|
612
|
+
const joinChannel = React4.useCallback(
|
|
609
613
|
(conv) => {
|
|
610
614
|
if (joinedConversationId.current === conv.id) return;
|
|
611
615
|
socket.joinConversation(conv.id, {
|
|
@@ -688,7 +692,7 @@ function useConversation({
|
|
|
688
692
|
},
|
|
689
693
|
[socket, config.userId]
|
|
690
694
|
);
|
|
691
|
-
|
|
695
|
+
React4.useEffect(() => {
|
|
692
696
|
let cancelled = false;
|
|
693
697
|
const init = async () => {
|
|
694
698
|
setIsLoading(true);
|
|
@@ -757,7 +761,7 @@ function useConversation({
|
|
|
757
761
|
setIsConnected(false);
|
|
758
762
|
};
|
|
759
763
|
}, [orderId]);
|
|
760
|
-
const ensureConversation =
|
|
764
|
+
const ensureConversation = React4.useCallback(
|
|
761
765
|
async (payload) => {
|
|
762
766
|
if (conversation) return { conv: conversation, persistedByCreate: false };
|
|
763
767
|
if (ensureConversationInFlight.current) {
|
|
@@ -811,7 +815,7 @@ function useConversation({
|
|
|
811
815
|
fetchMessages
|
|
812
816
|
]
|
|
813
817
|
);
|
|
814
|
-
const sendMessage =
|
|
818
|
+
const sendMessage = React4.useCallback(
|
|
815
819
|
async (body) => {
|
|
816
820
|
const payload = {
|
|
817
821
|
body,
|
|
@@ -855,7 +859,7 @@ function useConversation({
|
|
|
855
859
|
},
|
|
856
860
|
[ensureConversation, socket, replyTo, config]
|
|
857
861
|
);
|
|
858
|
-
const sendAudioMessage =
|
|
862
|
+
const sendAudioMessage = React4.useCallback(
|
|
859
863
|
async (audioBlob, duration) => {
|
|
860
864
|
if (!config.onUploadFile) return;
|
|
861
865
|
const url = await config.onUploadFile(audioBlob, "voice-message.webm");
|
|
@@ -871,7 +875,7 @@ function useConversation({
|
|
|
871
875
|
},
|
|
872
876
|
[config, ensureConversation, socket]
|
|
873
877
|
);
|
|
874
|
-
const sendFileMessage =
|
|
878
|
+
const sendFileMessage = React4.useCallback(
|
|
875
879
|
async (file) => {
|
|
876
880
|
if (!config.onUploadFile) return;
|
|
877
881
|
const url = await config.onUploadFile(file, file.name);
|
|
@@ -888,7 +892,7 @@ function useConversation({
|
|
|
888
892
|
},
|
|
889
893
|
[config, ensureConversation, socket]
|
|
890
894
|
);
|
|
891
|
-
const sendDeepLink =
|
|
895
|
+
const sendDeepLink = React4.useCallback(
|
|
892
896
|
async (path, label) => {
|
|
893
897
|
const payload = {
|
|
894
898
|
body: label,
|
|
@@ -901,14 +905,14 @@ function useConversation({
|
|
|
901
905
|
},
|
|
902
906
|
[ensureConversation, socket]
|
|
903
907
|
);
|
|
904
|
-
const sendTypingIndicator =
|
|
908
|
+
const sendTypingIndicator = React4.useCallback(
|
|
905
909
|
(isTyping) => {
|
|
906
910
|
if (!conversation || !isConnected) return;
|
|
907
911
|
socket.sendTyping(conversation.id, isTyping);
|
|
908
912
|
},
|
|
909
913
|
[conversation, isConnected, socket]
|
|
910
914
|
);
|
|
911
|
-
const markAsRead =
|
|
915
|
+
const markAsRead = React4.useCallback(
|
|
912
916
|
(messageId) => {
|
|
913
917
|
if (!conversation || !isConnected) return;
|
|
914
918
|
if (markedReadIdsRef.current.has(messageId)) return;
|
|
@@ -917,7 +921,7 @@ function useConversation({
|
|
|
917
921
|
},
|
|
918
922
|
[conversation, isConnected, socket]
|
|
919
923
|
);
|
|
920
|
-
const loadMoreMessages =
|
|
924
|
+
const loadMoreMessages = React4.useCallback(async () => {
|
|
921
925
|
if (!conversation || !hasMore || isLoading) return;
|
|
922
926
|
const oldestMessage = messages[0];
|
|
923
927
|
const before = oldestMessage?.id;
|
|
@@ -936,7 +940,7 @@ function useConversation({
|
|
|
936
940
|
setIsLoading(false);
|
|
937
941
|
}
|
|
938
942
|
}, [conversation, hasMore, isLoading, messages, fetchMessages, config]);
|
|
939
|
-
const pinMessage =
|
|
943
|
+
const pinMessage = React4.useCallback(
|
|
940
944
|
async (messageId) => {
|
|
941
945
|
if (!conversation) return;
|
|
942
946
|
try {
|
|
@@ -951,7 +955,7 @@ function useConversation({
|
|
|
951
955
|
},
|
|
952
956
|
[conversation, pinMessageApi, config]
|
|
953
957
|
);
|
|
954
|
-
const unpinMessage =
|
|
958
|
+
const unpinMessage = React4.useCallback(
|
|
955
959
|
async (messageId) => {
|
|
956
960
|
if (!conversation) return;
|
|
957
961
|
try {
|
|
@@ -991,10 +995,10 @@ function useConversation({
|
|
|
991
995
|
}
|
|
992
996
|
function useChannelSettings({ conversationId }) {
|
|
993
997
|
const { socket, config } = useCollab();
|
|
994
|
-
const [isUpdating, setIsUpdating] =
|
|
995
|
-
const [error, setError] =
|
|
998
|
+
const [isUpdating, setIsUpdating] = React4.useState(false);
|
|
999
|
+
const [error, setError] = React4.useState(null);
|
|
996
1000
|
const isAdmin = config.userRole === "admin";
|
|
997
|
-
const withLoading =
|
|
1001
|
+
const withLoading = React4.useCallback(
|
|
998
1002
|
async (operation) => {
|
|
999
1003
|
if (!conversationId) return;
|
|
1000
1004
|
setIsUpdating(true);
|
|
@@ -1013,25 +1017,25 @@ function useChannelSettings({ conversationId }) {
|
|
|
1013
1017
|
},
|
|
1014
1018
|
[conversationId, config]
|
|
1015
1019
|
);
|
|
1016
|
-
const inviteUser =
|
|
1020
|
+
const inviteUser = React4.useCallback(
|
|
1017
1021
|
async (payload) => {
|
|
1018
1022
|
await withLoading(() => socket.inviteUser(conversationId, payload));
|
|
1019
1023
|
},
|
|
1020
1024
|
[conversationId, socket, withLoading]
|
|
1021
1025
|
);
|
|
1022
|
-
const removeUser =
|
|
1026
|
+
const removeUser = React4.useCallback(
|
|
1023
1027
|
async (userId) => {
|
|
1024
1028
|
await withLoading(() => socket.removeUser(conversationId, userId));
|
|
1025
1029
|
},
|
|
1026
1030
|
[conversationId, socket, withLoading]
|
|
1027
1031
|
);
|
|
1028
|
-
const leaveChannel =
|
|
1032
|
+
const leaveChannel = React4.useCallback(async () => {
|
|
1029
1033
|
await withLoading(async () => {
|
|
1030
1034
|
await socket.removeUser(conversationId, config.userId);
|
|
1031
1035
|
socket.leaveConversation(conversationId);
|
|
1032
1036
|
});
|
|
1033
1037
|
}, [conversationId, socket, config.userId, withLoading]);
|
|
1034
|
-
const deleteChannel =
|
|
1038
|
+
const deleteChannel = React4.useCallback(async () => {
|
|
1035
1039
|
if (!isAdmin) {
|
|
1036
1040
|
const err = {
|
|
1037
1041
|
code: "FORBIDDEN",
|
|
@@ -1043,7 +1047,7 @@ function useChannelSettings({ conversationId }) {
|
|
|
1043
1047
|
}
|
|
1044
1048
|
await withLoading(() => socket.deleteChannel(conversationId));
|
|
1045
1049
|
}, [conversationId, socket, isAdmin, config, withLoading]);
|
|
1046
|
-
const updateChannel =
|
|
1050
|
+
const updateChannel = React4.useCallback(
|
|
1047
1051
|
async (payload) => {
|
|
1048
1052
|
await withLoading(() => socket.updateChannel(conversationId, payload));
|
|
1049
1053
|
},
|
|
@@ -1062,7 +1066,7 @@ function useChannelSettings({ conversationId }) {
|
|
|
1062
1066
|
}
|
|
1063
1067
|
function useDeepLinks() {
|
|
1064
1068
|
const { config } = useCollab();
|
|
1065
|
-
const parseDeepLink =
|
|
1069
|
+
const parseDeepLink = React4.useCallback((link) => {
|
|
1066
1070
|
if (!link.startsWith(DEEP_LINK_PREFIX)) return null;
|
|
1067
1071
|
const path = link.slice(DEEP_LINK_PREFIX.length);
|
|
1068
1072
|
const parts = path.split("/").filter(Boolean);
|
|
@@ -1074,7 +1078,7 @@ function useDeepLinks() {
|
|
|
1074
1078
|
id: parts[1]
|
|
1075
1079
|
};
|
|
1076
1080
|
}, []);
|
|
1077
|
-
const handleDeepLink =
|
|
1081
|
+
const handleDeepLink = React4.useCallback(
|
|
1078
1082
|
(link) => {
|
|
1079
1083
|
const parsed = parseDeepLink(link);
|
|
1080
1084
|
if (!parsed) return;
|
|
@@ -1088,10 +1092,10 @@ function useDeepLinks() {
|
|
|
1088
1092
|
},
|
|
1089
1093
|
[parseDeepLink, config]
|
|
1090
1094
|
);
|
|
1091
|
-
const containsDeepLink =
|
|
1095
|
+
const containsDeepLink = React4.useCallback((text) => {
|
|
1092
1096
|
return text.includes(DEEP_LINK_PREFIX);
|
|
1093
1097
|
}, []);
|
|
1094
|
-
const extractDeepLinks =
|
|
1098
|
+
const extractDeepLinks = React4.useCallback(
|
|
1095
1099
|
(text) => {
|
|
1096
1100
|
const regex = new RegExp(`${escapeRegex(DEEP_LINK_PREFIX)}[\\w/.-]+`, "g");
|
|
1097
1101
|
const matches = text.match(regex) || [];
|
|
@@ -1281,7 +1285,7 @@ function SeenByIndicator({
|
|
|
1281
1285
|
senderId,
|
|
1282
1286
|
className
|
|
1283
1287
|
}) {
|
|
1284
|
-
const [expanded, setExpanded] =
|
|
1288
|
+
const [expanded, setExpanded] = React4.useState(false);
|
|
1285
1289
|
if (senderId !== currentUserId) return null;
|
|
1286
1290
|
const seenByOthers = readBy.filter((id) => id !== currentUserId && id !== senderId).map((id) => participants.find((p) => p.userId === id)).filter((p) => !!p);
|
|
1287
1291
|
if (seenByOthers.length === 0) return null;
|
|
@@ -1441,7 +1445,7 @@ function MessageBubble({
|
|
|
1441
1445
|
pinDisabled = false,
|
|
1442
1446
|
className
|
|
1443
1447
|
}) {
|
|
1444
|
-
const [isHovered, setIsHovered] =
|
|
1448
|
+
const [isHovered, setIsHovered] = React4.useState(false);
|
|
1445
1449
|
if (message.type === "system") {
|
|
1446
1450
|
return /* @__PURE__ */ jsxRuntime.jsx(SystemBubble, { message });
|
|
1447
1451
|
}
|
|
@@ -1757,7 +1761,83 @@ var styles5 = {
|
|
|
1757
1761
|
color: "#d1d5db"
|
|
1758
1762
|
}
|
|
1759
1763
|
};
|
|
1760
|
-
|
|
1764
|
+
|
|
1765
|
+
// src/core/dateLabels.ts
|
|
1766
|
+
var SHORT_MONTHS = [
|
|
1767
|
+
"Jan",
|
|
1768
|
+
"Feb",
|
|
1769
|
+
"Mar",
|
|
1770
|
+
"Apr",
|
|
1771
|
+
"May",
|
|
1772
|
+
"Jun",
|
|
1773
|
+
"Jul",
|
|
1774
|
+
"Aug",
|
|
1775
|
+
"Sep",
|
|
1776
|
+
"Oct",
|
|
1777
|
+
"Nov",
|
|
1778
|
+
"Dec"
|
|
1779
|
+
];
|
|
1780
|
+
var LONG_WEEKDAYS = [
|
|
1781
|
+
"Sunday",
|
|
1782
|
+
"Monday",
|
|
1783
|
+
"Tuesday",
|
|
1784
|
+
"Wednesday",
|
|
1785
|
+
"Thursday",
|
|
1786
|
+
"Friday",
|
|
1787
|
+
"Saturday"
|
|
1788
|
+
];
|
|
1789
|
+
var ONE_DAY_MS = 864e5;
|
|
1790
|
+
function parseSafe(iso) {
|
|
1791
|
+
const d = new Date(iso);
|
|
1792
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
1793
|
+
}
|
|
1794
|
+
function startOfDay(d) {
|
|
1795
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
1796
|
+
}
|
|
1797
|
+
function daysBetween(a, b) {
|
|
1798
|
+
return Math.floor((startOfDay(a).getTime() - startOfDay(b).getTime()) / ONE_DAY_MS);
|
|
1799
|
+
}
|
|
1800
|
+
function formatFullDate(d) {
|
|
1801
|
+
return `${d.getDate()} ${SHORT_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
|
|
1802
|
+
}
|
|
1803
|
+
function formatLocaleTime(d) {
|
|
1804
|
+
try {
|
|
1805
|
+
return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
1806
|
+
} catch {
|
|
1807
|
+
const h = d.getHours();
|
|
1808
|
+
const m = d.getMinutes().toString().padStart(2, "0");
|
|
1809
|
+
const suffix = h >= 12 ? "PM" : "AM";
|
|
1810
|
+
const h12 = (h + 11) % 12 + 1;
|
|
1811
|
+
return `${h12}:${m} ${suffix}`;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function formatInboxTimestamp(iso) {
|
|
1815
|
+
if (!iso) return "";
|
|
1816
|
+
const d = parseSafe(iso);
|
|
1817
|
+
if (!d) return "";
|
|
1818
|
+
const diffDays = daysBetween(/* @__PURE__ */ new Date(), d);
|
|
1819
|
+
if (diffDays === 0) return formatLocaleTime(d);
|
|
1820
|
+
if (diffDays === 1) return "Yesterday";
|
|
1821
|
+
if (diffDays > 1 && diffDays < 7) return LONG_WEEKDAYS[d.getDay()];
|
|
1822
|
+
return formatFullDate(d);
|
|
1823
|
+
}
|
|
1824
|
+
function formatDayDivider(iso) {
|
|
1825
|
+
if (!iso) return "";
|
|
1826
|
+
const d = parseSafe(iso);
|
|
1827
|
+
if (!d) return "";
|
|
1828
|
+
const diffDays = daysBetween(/* @__PURE__ */ new Date(), d);
|
|
1829
|
+
if (diffDays === 0) return "Today";
|
|
1830
|
+
if (diffDays === 1) return "Yesterday";
|
|
1831
|
+
if (diffDays > 1 && diffDays < 7) return LONG_WEEKDAYS[d.getDay()];
|
|
1832
|
+
return formatFullDate(d);
|
|
1833
|
+
}
|
|
1834
|
+
function dayBucketKey(iso) {
|
|
1835
|
+
if (!iso) return "";
|
|
1836
|
+
const d = parseSafe(iso);
|
|
1837
|
+
if (!d) return "";
|
|
1838
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
1839
|
+
}
|
|
1840
|
+
var MessageList = React4.forwardRef(function MessageList2({
|
|
1761
1841
|
messages,
|
|
1762
1842
|
currentUserId,
|
|
1763
1843
|
participants = [],
|
|
@@ -1775,10 +1855,10 @@ var MessageList = react.forwardRef(function MessageList2({
|
|
|
1775
1855
|
pinDisabled,
|
|
1776
1856
|
className
|
|
1777
1857
|
}, ref) {
|
|
1778
|
-
const containerRef =
|
|
1779
|
-
const bottomRef =
|
|
1780
|
-
const prevMessageCount =
|
|
1781
|
-
|
|
1858
|
+
const containerRef = React4.useRef(null);
|
|
1859
|
+
const bottomRef = React4.useRef(null);
|
|
1860
|
+
const prevMessageCount = React4.useRef(messages.length);
|
|
1861
|
+
React4.useImperativeHandle(
|
|
1782
1862
|
ref,
|
|
1783
1863
|
() => ({
|
|
1784
1864
|
scrollToMessage: (messageId) => {
|
|
@@ -1795,7 +1875,7 @@ var MessageList = react.forwardRef(function MessageList2({
|
|
|
1795
1875
|
}),
|
|
1796
1876
|
[]
|
|
1797
1877
|
);
|
|
1798
|
-
|
|
1878
|
+
React4.useEffect(() => {
|
|
1799
1879
|
if (messages.length > prevMessageCount.current) {
|
|
1800
1880
|
const lastMessage = messages[messages.length - 1];
|
|
1801
1881
|
const isOwnMessage = lastMessage?.senderId === currentUserId;
|
|
@@ -1805,7 +1885,7 @@ var MessageList = react.forwardRef(function MessageList2({
|
|
|
1805
1885
|
}
|
|
1806
1886
|
prevMessageCount.current = messages.length;
|
|
1807
1887
|
}, [messages, currentUserId]);
|
|
1808
|
-
|
|
1888
|
+
React4.useEffect(() => {
|
|
1809
1889
|
if (!onMessageVisible || !containerRef.current) return;
|
|
1810
1890
|
const observer = new IntersectionObserver(
|
|
1811
1891
|
(entries) => {
|
|
@@ -1822,7 +1902,7 @@ var MessageList = react.forwardRef(function MessageList2({
|
|
|
1822
1902
|
messageElements.forEach((el) => observer.observe(el));
|
|
1823
1903
|
return () => observer.disconnect();
|
|
1824
1904
|
}, [messages, onMessageVisible]);
|
|
1825
|
-
const handleScroll =
|
|
1905
|
+
const handleScroll = React4.useCallback(() => {
|
|
1826
1906
|
if (!containerRef.current || !hasMore || isLoading) return;
|
|
1827
1907
|
if (containerRef.current.scrollTop < 50) {
|
|
1828
1908
|
onLoadMore();
|
|
@@ -1842,22 +1922,30 @@ var MessageList = react.forwardRef(function MessageList2({
|
|
|
1842
1922
|
/* @__PURE__ */ jsxRuntime.jsx("p", { style: styles6.emptyTitle, children: "No messages yet" }),
|
|
1843
1923
|
/* @__PURE__ */ jsxRuntime.jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation about this study" })
|
|
1844
1924
|
] }),
|
|
1845
|
-
messages.map((message) =>
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1925
|
+
messages.map((message, index) => {
|
|
1926
|
+
const currentBucket = dayBucketKey(message.insertedAt);
|
|
1927
|
+
const prevBucket = index === 0 ? null : dayBucketKey(messages[index - 1].insertedAt);
|
|
1928
|
+
const showDivider = currentBucket && currentBucket !== prevBucket;
|
|
1929
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(React4__default.default.Fragment, { children: [
|
|
1930
|
+
showDivider && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
|
|
1931
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1932
|
+
MessageBubble,
|
|
1933
|
+
{
|
|
1934
|
+
message,
|
|
1935
|
+
isOwn: message.senderId === currentUserId,
|
|
1936
|
+
participants,
|
|
1937
|
+
currentUserId,
|
|
1938
|
+
showSeenBy,
|
|
1939
|
+
onDeepLinkClick,
|
|
1940
|
+
onReply,
|
|
1941
|
+
onPin,
|
|
1942
|
+
onUnpin,
|
|
1943
|
+
onCopy,
|
|
1944
|
+
pinDisabled
|
|
1945
|
+
}
|
|
1946
|
+
) })
|
|
1947
|
+
] }, message.id);
|
|
1948
|
+
}),
|
|
1861
1949
|
typingUsers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles6.typingIndicator, children: [
|
|
1862
1950
|
typingUsers.map((t) => t.userName ?? "Someone").join(", "),
|
|
1863
1951
|
typingUsers.length === 1 ? " is " : " are ",
|
|
@@ -1919,109 +2007,24 @@ var styles6 = {
|
|
|
1919
2007
|
fontSize: "12px",
|
|
1920
2008
|
color: "#9ca3af",
|
|
1921
2009
|
fontStyle: "italic"
|
|
2010
|
+
},
|
|
2011
|
+
dayDivider: {
|
|
2012
|
+
display: "flex",
|
|
2013
|
+
justifyContent: "center",
|
|
2014
|
+
margin: "12px 0 8px"
|
|
2015
|
+
},
|
|
2016
|
+
dayDividerPill: {
|
|
2017
|
+
display: "inline-block",
|
|
2018
|
+
padding: "4px 12px",
|
|
2019
|
+
fontSize: "12px",
|
|
2020
|
+
fontWeight: 500,
|
|
2021
|
+
color: "#475569",
|
|
2022
|
+
backgroundColor: "#f1f5f9",
|
|
2023
|
+
border: "1px solid #e2e8f0",
|
|
2024
|
+
borderRadius: "12px",
|
|
2025
|
+
letterSpacing: "0.2px"
|
|
1922
2026
|
}
|
|
1923
2027
|
};
|
|
1924
|
-
function useAudioRecorder() {
|
|
1925
|
-
const [isRecording, setIsRecording] = react.useState(false);
|
|
1926
|
-
const [duration, setDuration] = react.useState(0);
|
|
1927
|
-
const [error, setError] = react.useState(null);
|
|
1928
|
-
const mediaRecorderRef = react.useRef(null);
|
|
1929
|
-
const chunksRef = react.useRef([]);
|
|
1930
|
-
const streamRef = react.useRef(null);
|
|
1931
|
-
const timerRef = react.useRef(null);
|
|
1932
|
-
const startTimeRef = react.useRef(0);
|
|
1933
|
-
const resolveRef = react.useRef(null);
|
|
1934
|
-
const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
|
|
1935
|
-
const cleanup = react.useCallback(() => {
|
|
1936
|
-
if (timerRef.current) {
|
|
1937
|
-
clearInterval(timerRef.current);
|
|
1938
|
-
timerRef.current = null;
|
|
1939
|
-
}
|
|
1940
|
-
if (streamRef.current) {
|
|
1941
|
-
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
1942
|
-
streamRef.current = null;
|
|
1943
|
-
}
|
|
1944
|
-
mediaRecorderRef.current = null;
|
|
1945
|
-
chunksRef.current = [];
|
|
1946
|
-
setIsRecording(false);
|
|
1947
|
-
setDuration(0);
|
|
1948
|
-
}, []);
|
|
1949
|
-
const start = react.useCallback(async () => {
|
|
1950
|
-
if (!isSupported) {
|
|
1951
|
-
setError("Audio recording is not supported in this browser");
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
try {
|
|
1955
|
-
setError(null);
|
|
1956
|
-
chunksRef.current = [];
|
|
1957
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1958
|
-
streamRef.current = stream;
|
|
1959
|
-
const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
|
|
1960
|
-
const recorder = new MediaRecorder(stream, { mimeType });
|
|
1961
|
-
mediaRecorderRef.current = recorder;
|
|
1962
|
-
recorder.ondataavailable = (event) => {
|
|
1963
|
-
if (event.data.size > 0) {
|
|
1964
|
-
chunksRef.current.push(event.data);
|
|
1965
|
-
}
|
|
1966
|
-
};
|
|
1967
|
-
recorder.onerror = () => {
|
|
1968
|
-
setError("Recording failed");
|
|
1969
|
-
cleanup();
|
|
1970
|
-
resolveRef.current?.(null);
|
|
1971
|
-
resolveRef.current = null;
|
|
1972
|
-
};
|
|
1973
|
-
recorder.onstop = () => {
|
|
1974
|
-
const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
1975
|
-
const blob = new Blob(chunksRef.current, { type: mimeType });
|
|
1976
|
-
cleanup();
|
|
1977
|
-
resolveRef.current?.({ blob, duration: finalDuration });
|
|
1978
|
-
resolveRef.current = null;
|
|
1979
|
-
};
|
|
1980
|
-
recorder.start(250);
|
|
1981
|
-
startTimeRef.current = Date.now();
|
|
1982
|
-
setIsRecording(true);
|
|
1983
|
-
timerRef.current = setInterval(() => {
|
|
1984
|
-
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
1985
|
-
setDuration(elapsed);
|
|
1986
|
-
}, 1e3);
|
|
1987
|
-
} catch (err) {
|
|
1988
|
-
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
1989
|
-
setError("Microphone access denied. Please allow microphone permissions.");
|
|
1990
|
-
} else {
|
|
1991
|
-
setError("Failed to start recording");
|
|
1992
|
-
}
|
|
1993
|
-
cleanup();
|
|
1994
|
-
}
|
|
1995
|
-
}, [isSupported, cleanup]);
|
|
1996
|
-
const stop = react.useCallback(async () => {
|
|
1997
|
-
return new Promise((resolve) => {
|
|
1998
|
-
if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
|
|
1999
|
-
resolve(null);
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
resolveRef.current = resolve;
|
|
2003
|
-
mediaRecorderRef.current.stop();
|
|
2004
|
-
});
|
|
2005
|
-
}, []);
|
|
2006
|
-
const cancel = react.useCallback(() => {
|
|
2007
|
-
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
2008
|
-
mediaRecorderRef.current.onstop = null;
|
|
2009
|
-
mediaRecorderRef.current.stop();
|
|
2010
|
-
}
|
|
2011
|
-
cleanup();
|
|
2012
|
-
resolveRef.current?.(null);
|
|
2013
|
-
resolveRef.current = null;
|
|
2014
|
-
}, [cleanup]);
|
|
2015
|
-
return {
|
|
2016
|
-
isRecording,
|
|
2017
|
-
duration,
|
|
2018
|
-
start,
|
|
2019
|
-
stop,
|
|
2020
|
-
cancel,
|
|
2021
|
-
isSupported,
|
|
2022
|
-
error
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
2028
|
function ReplyPreview({ message, onCancel, className }) {
|
|
2026
2029
|
const preview = renderQuotedPreview(message);
|
|
2027
2030
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles7.container, children: [
|
|
@@ -2114,23 +2117,10 @@ var styles7 = {
|
|
|
2114
2117
|
flexShrink: 0
|
|
2115
2118
|
}
|
|
2116
2119
|
};
|
|
2117
|
-
function MicIcon({ size = 20, color = "currentColor" }) {
|
|
2118
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2119
|
-
"svg",
|
|
2120
|
-
{
|
|
2121
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
2122
|
-
width: size,
|
|
2123
|
-
height: size,
|
|
2124
|
-
viewBox: "0 0 24 24",
|
|
2125
|
-
fill: color,
|
|
2126
|
-
"aria-hidden": "true",
|
|
2127
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" })
|
|
2128
|
-
}
|
|
2129
|
-
);
|
|
2130
|
-
}
|
|
2131
2120
|
function MessageInput({
|
|
2132
2121
|
onSendText,
|
|
2133
|
-
onSendAudio
|
|
2122
|
+
// MIC DISABLED — onSendAudio still accepted but unused.
|
|
2123
|
+
// onSendAudio,
|
|
2134
2124
|
onSendFile,
|
|
2135
2125
|
onTyping,
|
|
2136
2126
|
replyTo,
|
|
@@ -2139,15 +2129,14 @@ function MessageInput({
|
|
|
2139
2129
|
placeholder = "Type a message...",
|
|
2140
2130
|
className
|
|
2141
2131
|
}) {
|
|
2142
|
-
const [text, setText] =
|
|
2143
|
-
const [isSending, setIsSending] =
|
|
2144
|
-
const [fileError, setFileError] =
|
|
2145
|
-
const fileInputRef =
|
|
2146
|
-
const typingTimeoutRef =
|
|
2147
|
-
const isTypingRef =
|
|
2148
|
-
const sendingRef =
|
|
2149
|
-
const
|
|
2150
|
-
const handleTyping = react.useCallback(() => {
|
|
2132
|
+
const [text, setText] = React4.useState("");
|
|
2133
|
+
const [isSending, setIsSending] = React4.useState(false);
|
|
2134
|
+
const [fileError, setFileError] = React4.useState(null);
|
|
2135
|
+
const fileInputRef = React4.useRef(null);
|
|
2136
|
+
const typingTimeoutRef = React4.useRef(null);
|
|
2137
|
+
const isTypingRef = React4.useRef(false);
|
|
2138
|
+
const sendingRef = React4.useRef(false);
|
|
2139
|
+
const handleTyping = React4.useCallback(() => {
|
|
2151
2140
|
if (!isTypingRef.current) {
|
|
2152
2141
|
isTypingRef.current = true;
|
|
2153
2142
|
onTyping(true);
|
|
@@ -2160,12 +2149,12 @@ function MessageInput({
|
|
|
2160
2149
|
onTyping(false);
|
|
2161
2150
|
}, 2e3);
|
|
2162
2151
|
}, [onTyping]);
|
|
2163
|
-
|
|
2152
|
+
React4.useEffect(() => {
|
|
2164
2153
|
return () => {
|
|
2165
2154
|
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
|
|
2166
2155
|
};
|
|
2167
2156
|
}, []);
|
|
2168
|
-
const handleSendText =
|
|
2157
|
+
const handleSendText = React4.useCallback(async () => {
|
|
2169
2158
|
const trimmed = text.trim();
|
|
2170
2159
|
if (!trimmed || sendingRef.current) return;
|
|
2171
2160
|
sendingRef.current = true;
|
|
@@ -2186,7 +2175,7 @@ function MessageInput({
|
|
|
2186
2175
|
setIsSending(false);
|
|
2187
2176
|
}
|
|
2188
2177
|
}, [text, onSendText, onTyping]);
|
|
2189
|
-
const handleKeyDown =
|
|
2178
|
+
const handleKeyDown = React4.useCallback(
|
|
2190
2179
|
(e) => {
|
|
2191
2180
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
2192
2181
|
e.preventDefault();
|
|
@@ -2195,18 +2184,7 @@ function MessageInput({
|
|
|
2195
2184
|
},
|
|
2196
2185
|
[handleSendText]
|
|
2197
2186
|
);
|
|
2198
|
-
const
|
|
2199
|
-
const result = await stopRecording();
|
|
2200
|
-
if (result) {
|
|
2201
|
-
setIsSending(true);
|
|
2202
|
-
try {
|
|
2203
|
-
await onSendAudio(result.blob, result.duration);
|
|
2204
|
-
} finally {
|
|
2205
|
-
setIsSending(false);
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
}, [stopRecording, onSendAudio]);
|
|
2209
|
-
const handleFileSelect = react.useCallback(
|
|
2187
|
+
const handleFileSelect = React4.useCallback(
|
|
2210
2188
|
async (e) => {
|
|
2211
2189
|
const file = e.target.files?.[0];
|
|
2212
2190
|
if (!file) return;
|
|
@@ -2227,29 +2205,8 @@ function MessageInput({
|
|
|
2227
2205
|
);
|
|
2228
2206
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles8.container, children: [
|
|
2229
2207
|
replyTo && onCancelReply && /* @__PURE__ */ jsxRuntime.jsx(ReplyPreview, { message: replyTo, onCancel: onCancelReply }),
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingIndicator, children: [
|
|
2233
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.recordingDot }),
|
|
2234
|
-
"Recording ",
|
|
2235
|
-
formatDuration2(duration)
|
|
2236
|
-
] }),
|
|
2237
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingActions, children: [
|
|
2238
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: cancelRecording, style: styles8.cancelButton, type: "button", children: "Cancel" }),
|
|
2239
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleAudioStop, style: styles8.stopButton, type: "button", children: "Send" })
|
|
2240
|
-
] })
|
|
2241
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.inputBar, children: [
|
|
2242
|
-
micSupported && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2243
|
-
"button",
|
|
2244
|
-
{
|
|
2245
|
-
onClick: startRecording,
|
|
2246
|
-
disabled: disabled || isSending,
|
|
2247
|
-
style: styles8.iconButton,
|
|
2248
|
-
title: "Record voice message",
|
|
2249
|
-
type: "button",
|
|
2250
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(MicIcon, { size: 20, color: "#374151" })
|
|
2251
|
-
}
|
|
2252
|
-
),
|
|
2208
|
+
fileError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles8.error, children: fileError }),
|
|
2209
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.inputBar, children: [
|
|
2253
2210
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2254
2211
|
"button",
|
|
2255
2212
|
{
|
|
@@ -2302,11 +2259,6 @@ function MessageInput({
|
|
|
2302
2259
|
] })
|
|
2303
2260
|
] });
|
|
2304
2261
|
}
|
|
2305
|
-
function formatDuration2(seconds) {
|
|
2306
|
-
const m = Math.floor(seconds / 60);
|
|
2307
|
-
const s = seconds % 60;
|
|
2308
|
-
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
2309
|
-
}
|
|
2310
2262
|
var styles8 = {
|
|
2311
2263
|
container: {
|
|
2312
2264
|
borderTop: "1px solid #e5e7eb",
|
|
@@ -2362,53 +2314,7 @@ var styles8 = {
|
|
|
2362
2314
|
borderRadius: "50%",
|
|
2363
2315
|
cursor: "pointer",
|
|
2364
2316
|
flexShrink: 0
|
|
2365
|
-
}
|
|
2366
|
-
recordingBar: {
|
|
2367
|
-
display: "flex",
|
|
2368
|
-
justifyContent: "space-between",
|
|
2369
|
-
alignItems: "center",
|
|
2370
|
-
padding: "12px 16px",
|
|
2371
|
-
backgroundColor: "#fef2f2"
|
|
2372
|
-
},
|
|
2373
|
-
recordingIndicator: {
|
|
2374
|
-
display: "flex",
|
|
2375
|
-
alignItems: "center",
|
|
2376
|
-
gap: "8px",
|
|
2377
|
-
fontSize: "14px",
|
|
2378
|
-
color: "#dc2626",
|
|
2379
|
-
fontWeight: 500
|
|
2380
|
-
},
|
|
2381
|
-
recordingDot: {
|
|
2382
|
-
width: "8px",
|
|
2383
|
-
height: "8px",
|
|
2384
|
-
borderRadius: "50%",
|
|
2385
|
-
backgroundColor: "#dc2626",
|
|
2386
|
-
animation: "pulse 1.5s infinite"
|
|
2387
|
-
},
|
|
2388
|
-
recordingActions: {
|
|
2389
|
-
display: "flex",
|
|
2390
|
-
gap: "8px"
|
|
2391
|
-
},
|
|
2392
|
-
cancelButton: {
|
|
2393
|
-
padding: "6px 14px",
|
|
2394
|
-
fontSize: "13px",
|
|
2395
|
-
color: "#6b7280",
|
|
2396
|
-
backgroundColor: "#ffffff",
|
|
2397
|
-
border: "1px solid #d1d5db",
|
|
2398
|
-
borderRadius: "6px",
|
|
2399
|
-
cursor: "pointer"
|
|
2400
|
-
},
|
|
2401
|
-
stopButton: {
|
|
2402
|
-
padding: "6px 14px",
|
|
2403
|
-
fontSize: "13px",
|
|
2404
|
-
fontWeight: 500,
|
|
2405
|
-
color: "#ffffff",
|
|
2406
|
-
backgroundColor: "#dc2626",
|
|
2407
|
-
border: "none",
|
|
2408
|
-
borderRadius: "6px",
|
|
2409
|
-
cursor: "pointer"
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2317
|
+
}};
|
|
2412
2318
|
function ParticipantsList({
|
|
2413
2319
|
participants,
|
|
2414
2320
|
currentUserId,
|
|
@@ -2574,13 +2480,13 @@ function ChannelSettings({
|
|
|
2574
2480
|
onClose,
|
|
2575
2481
|
className
|
|
2576
2482
|
}) {
|
|
2577
|
-
const [editingName, setEditingName] =
|
|
2578
|
-
const [nameValue, setNameValue] =
|
|
2579
|
-
const [showInvite, setShowInvite] =
|
|
2580
|
-
const [inviteId, setInviteId] =
|
|
2581
|
-
const [inviteName, setInviteName] =
|
|
2582
|
-
const [inviteRole, setInviteRole] =
|
|
2583
|
-
const [confirmDelete, setConfirmDelete] =
|
|
2483
|
+
const [editingName, setEditingName] = React4.useState(false);
|
|
2484
|
+
const [nameValue, setNameValue] = React4.useState(channelName);
|
|
2485
|
+
const [showInvite, setShowInvite] = React4.useState(false);
|
|
2486
|
+
const [inviteId, setInviteId] = React4.useState("");
|
|
2487
|
+
const [inviteName, setInviteName] = React4.useState("");
|
|
2488
|
+
const [inviteRole, setInviteRole] = React4.useState("radiologist");
|
|
2489
|
+
const [confirmDelete, setConfirmDelete] = React4.useState(false);
|
|
2584
2490
|
const handleSaveName = async () => {
|
|
2585
2491
|
if (nameValue.trim() && nameValue !== channelName) {
|
|
2586
2492
|
await onUpdateChannel({ name: nameValue.trim() });
|
|
@@ -2871,7 +2777,7 @@ function PinnedMessagesBar({
|
|
|
2871
2777
|
onUnpin,
|
|
2872
2778
|
className
|
|
2873
2779
|
}) {
|
|
2874
|
-
const [index, setIndex] =
|
|
2780
|
+
const [index, setIndex] = React4.useState(0);
|
|
2875
2781
|
if (pinnedMessages.length === 0) return null;
|
|
2876
2782
|
const safeIndex = Math.min(index, pinnedMessages.length - 1);
|
|
2877
2783
|
const current = pinnedMessages[safeIndex];
|
|
@@ -3030,8 +2936,8 @@ function CollabPanel({
|
|
|
3030
2936
|
style
|
|
3031
2937
|
}) {
|
|
3032
2938
|
const { config } = useCollab();
|
|
3033
|
-
const [showSettings, setShowSettings] =
|
|
3034
|
-
const messageListRef =
|
|
2939
|
+
const [showSettings, setShowSettings] = React4.useState(false);
|
|
2940
|
+
const messageListRef = React4.useRef(null);
|
|
3035
2941
|
const {
|
|
3036
2942
|
conversation,
|
|
3037
2943
|
messages,
|
|
@@ -3203,17 +3109,17 @@ function CollabPopup({
|
|
|
3203
3109
|
height = 520,
|
|
3204
3110
|
className
|
|
3205
3111
|
}) {
|
|
3206
|
-
const [position, setPosition] =
|
|
3112
|
+
const [position, setPosition] = React4.useState(
|
|
3207
3113
|
initialPosition || {
|
|
3208
3114
|
x: window.innerWidth - width - 24,
|
|
3209
3115
|
y: Math.max(24, window.innerHeight - height - 104)
|
|
3210
3116
|
}
|
|
3211
3117
|
);
|
|
3212
|
-
const [isDragging, setIsDragging] =
|
|
3213
|
-
const [isMinimized, setIsMinimized] =
|
|
3214
|
-
const dragOffset =
|
|
3215
|
-
const containerRef =
|
|
3216
|
-
const handleMouseDown =
|
|
3118
|
+
const [isDragging, setIsDragging] = React4.useState(false);
|
|
3119
|
+
const [isMinimized, setIsMinimized] = React4.useState(false);
|
|
3120
|
+
const dragOffset = React4.useRef({ x: 0, y: 0 });
|
|
3121
|
+
const containerRef = React4.useRef(null);
|
|
3122
|
+
const handleMouseDown = React4.useCallback(
|
|
3217
3123
|
(e) => {
|
|
3218
3124
|
if (!e.target.closest("[data-drag-handle]")) return;
|
|
3219
3125
|
setIsDragging(true);
|
|
@@ -3225,7 +3131,7 @@ function CollabPopup({
|
|
|
3225
3131
|
},
|
|
3226
3132
|
[position]
|
|
3227
3133
|
);
|
|
3228
|
-
|
|
3134
|
+
React4.useEffect(() => {
|
|
3229
3135
|
if (!isOpen) return;
|
|
3230
3136
|
const handleKey = (e) => {
|
|
3231
3137
|
if (e.key === "Escape") {
|
|
@@ -3235,7 +3141,7 @@ function CollabPopup({
|
|
|
3235
3141
|
window.addEventListener("keydown", handleKey);
|
|
3236
3142
|
return () => window.removeEventListener("keydown", handleKey);
|
|
3237
3143
|
}, [isOpen, onClose]);
|
|
3238
|
-
|
|
3144
|
+
React4.useEffect(() => {
|
|
3239
3145
|
if (!isDragging) return;
|
|
3240
3146
|
const handleMouseMove = (e) => {
|
|
3241
3147
|
setPosition({
|
|
@@ -3358,25 +3264,25 @@ function useInlineCollab({
|
|
|
3358
3264
|
invalidatePreview,
|
|
3359
3265
|
createConversationWithMessage
|
|
3360
3266
|
} = useCollab();
|
|
3361
|
-
const [preview, setPreview] =
|
|
3362
|
-
const [messages, setMessages] =
|
|
3363
|
-
const [participants, setParticipants] =
|
|
3364
|
-
const [unreadCount, setUnreadCount] =
|
|
3365
|
-
const [isLoading, setIsLoading] =
|
|
3366
|
-
const [isSubscribed, setIsSubscribed] =
|
|
3367
|
-
const [error, setError] =
|
|
3368
|
-
const elementRef =
|
|
3369
|
-
const observerRef =
|
|
3370
|
-
const subscribedConversationIdRef =
|
|
3371
|
-
const trimToLimit =
|
|
3267
|
+
const [preview, setPreview] = React4.useState(null);
|
|
3268
|
+
const [messages, setMessages] = React4.useState([]);
|
|
3269
|
+
const [participants, setParticipants] = React4.useState([]);
|
|
3270
|
+
const [unreadCount, setUnreadCount] = React4.useState(0);
|
|
3271
|
+
const [isLoading, setIsLoading] = React4.useState(true);
|
|
3272
|
+
const [isSubscribed, setIsSubscribed] = React4.useState(false);
|
|
3273
|
+
const [error, setError] = React4.useState(null);
|
|
3274
|
+
const elementRef = React4.useRef(null);
|
|
3275
|
+
const observerRef = React4.useRef(null);
|
|
3276
|
+
const subscribedConversationIdRef = React4.useRef(null);
|
|
3277
|
+
const trimToLimit = React4.useCallback(
|
|
3372
3278
|
(msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
|
|
3373
3279
|
[messageLimit]
|
|
3374
3280
|
);
|
|
3375
|
-
const buildName =
|
|
3281
|
+
const buildName = React4.useCallback(
|
|
3376
3282
|
() => buildChannelName(patientData),
|
|
3377
3283
|
[patientData]
|
|
3378
3284
|
);
|
|
3379
|
-
|
|
3285
|
+
React4.useEffect(() => {
|
|
3380
3286
|
let cancelled = false;
|
|
3381
3287
|
const load = async () => {
|
|
3382
3288
|
setIsLoading(true);
|
|
@@ -3402,7 +3308,7 @@ function useInlineCollab({
|
|
|
3402
3308
|
cancelled = true;
|
|
3403
3309
|
};
|
|
3404
3310
|
}, [orderId, requestPreview, trimToLimit]);
|
|
3405
|
-
const subscribe =
|
|
3311
|
+
const subscribe = React4.useCallback(
|
|
3406
3312
|
(conversationId) => {
|
|
3407
3313
|
if (subscribedConversationIdRef.current === conversationId) return;
|
|
3408
3314
|
socket.joinConversation(conversationId, {
|
|
@@ -3426,7 +3332,7 @@ function useInlineCollab({
|
|
|
3426
3332
|
},
|
|
3427
3333
|
[socket, trimToLimit, config.userId]
|
|
3428
3334
|
);
|
|
3429
|
-
const unsubscribe =
|
|
3335
|
+
const unsubscribe = React4.useCallback(() => {
|
|
3430
3336
|
const convId = subscribedConversationIdRef.current;
|
|
3431
3337
|
if (convId) {
|
|
3432
3338
|
socket.leaveConversation(convId);
|
|
@@ -3434,7 +3340,7 @@ function useInlineCollab({
|
|
|
3434
3340
|
setIsSubscribed(false);
|
|
3435
3341
|
}
|
|
3436
3342
|
}, [socket]);
|
|
3437
|
-
const containerRef =
|
|
3343
|
+
const containerRef = React4.useCallback(
|
|
3438
3344
|
(element) => {
|
|
3439
3345
|
if (observerRef.current) {
|
|
3440
3346
|
observerRef.current.disconnect();
|
|
@@ -3460,18 +3366,18 @@ function useInlineCollab({
|
|
|
3460
3366
|
},
|
|
3461
3367
|
[preview, subscribe, unsubscribe]
|
|
3462
3368
|
);
|
|
3463
|
-
|
|
3369
|
+
React4.useEffect(() => {
|
|
3464
3370
|
return () => {
|
|
3465
3371
|
if (observerRef.current) observerRef.current.disconnect();
|
|
3466
3372
|
unsubscribe();
|
|
3467
3373
|
};
|
|
3468
3374
|
}, [unsubscribe]);
|
|
3469
|
-
|
|
3375
|
+
React4.useEffect(() => {
|
|
3470
3376
|
if (preview?.conversationId && elementRef.current && !observerRef.current) {
|
|
3471
3377
|
containerRef(elementRef.current);
|
|
3472
3378
|
}
|
|
3473
3379
|
}, [preview, containerRef]);
|
|
3474
|
-
const dispatchSend =
|
|
3380
|
+
const dispatchSend = React4.useCallback(
|
|
3475
3381
|
async (payload) => {
|
|
3476
3382
|
if (!preview) {
|
|
3477
3383
|
try {
|
|
@@ -3528,7 +3434,7 @@ function useInlineCollab({
|
|
|
3528
3434
|
config
|
|
3529
3435
|
]
|
|
3530
3436
|
);
|
|
3531
|
-
const sendMessage =
|
|
3437
|
+
const sendMessage = React4.useCallback(
|
|
3532
3438
|
async (body) => {
|
|
3533
3439
|
const trimmed = body.trim();
|
|
3534
3440
|
if (!trimmed) return;
|
|
@@ -3536,7 +3442,7 @@ function useInlineCollab({
|
|
|
3536
3442
|
},
|
|
3537
3443
|
[dispatchSend]
|
|
3538
3444
|
);
|
|
3539
|
-
const sendAudioMessage =
|
|
3445
|
+
const sendAudioMessage = React4.useCallback(
|
|
3540
3446
|
async (audioBlob, duration) => {
|
|
3541
3447
|
if (!config.onUploadFile) {
|
|
3542
3448
|
setError("File upload is not configured");
|
|
@@ -3649,10 +3555,10 @@ function RoleDot({ role }) {
|
|
|
3649
3555
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.roleDot, backgroundColor: colors[role] } });
|
|
3650
3556
|
}
|
|
3651
3557
|
function InlineAudioPlayer({ url, duration }) {
|
|
3652
|
-
const audioRef =
|
|
3653
|
-
const [isPlaying, setIsPlaying] =
|
|
3654
|
-
const [currentTime, setCurrentTime] =
|
|
3655
|
-
const toggle =
|
|
3558
|
+
const audioRef = React4.useRef(null);
|
|
3559
|
+
const [isPlaying, setIsPlaying] = React4.useState(false);
|
|
3560
|
+
const [currentTime, setCurrentTime] = React4.useState(0);
|
|
3561
|
+
const toggle = React4.useCallback(
|
|
3656
3562
|
(e) => {
|
|
3657
3563
|
e.stopPropagation();
|
|
3658
3564
|
const audio = audioRef.current;
|
|
@@ -3666,7 +3572,7 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
3666
3572
|
},
|
|
3667
3573
|
[isPlaying]
|
|
3668
3574
|
);
|
|
3669
|
-
|
|
3575
|
+
React4.useEffect(() => {
|
|
3670
3576
|
const audio = audioRef.current;
|
|
3671
3577
|
if (!audio) return;
|
|
3672
3578
|
const onPlay = () => setIsPlaying(true);
|
|
@@ -3710,7 +3616,7 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
3710
3616
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "70%" } }),
|
|
3711
3617
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "40%" } })
|
|
3712
3618
|
] }),
|
|
3713
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.audioDuration, children:
|
|
3619
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.audioDuration, children: formatDuration2(displayTime) }),
|
|
3714
3620
|
/* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
|
|
3715
3621
|
] });
|
|
3716
3622
|
}
|
|
@@ -3726,7 +3632,7 @@ function renderMessagePreview(message) {
|
|
|
3726
3632
|
return message.body;
|
|
3727
3633
|
}
|
|
3728
3634
|
}
|
|
3729
|
-
function
|
|
3635
|
+
function formatDuration2(seconds) {
|
|
3730
3636
|
const safe = Math.max(0, Math.floor(seconds));
|
|
3731
3637
|
const m = Math.floor(safe / 60);
|
|
3732
3638
|
const s = safe % 60;
|
|
@@ -3734,26 +3640,18 @@ function formatDuration3(seconds) {
|
|
|
3734
3640
|
}
|
|
3735
3641
|
function InlineInputBar({
|
|
3736
3642
|
onSend,
|
|
3737
|
-
onSendAudio
|
|
3643
|
+
// MIC DISABLED — onSendAudio accepted but unused.
|
|
3644
|
+
// onSendAudio,
|
|
3738
3645
|
placeholder,
|
|
3739
3646
|
unreadCount,
|
|
3740
3647
|
isLive,
|
|
3741
3648
|
showExpand,
|
|
3742
3649
|
onExpand
|
|
3743
3650
|
}) {
|
|
3744
|
-
const [text, setText] =
|
|
3745
|
-
const [isSending, setIsSending] =
|
|
3746
|
-
const inputRef =
|
|
3747
|
-
const {
|
|
3748
|
-
isRecording,
|
|
3749
|
-
duration,
|
|
3750
|
-
start: startRecording,
|
|
3751
|
-
stop: stopRecording,
|
|
3752
|
-
cancel: cancelRecording,
|
|
3753
|
-
isSupported: micSupported,
|
|
3754
|
-
error: micError
|
|
3755
|
-
} = useAudioRecorder();
|
|
3756
|
-
const handleSend = react.useCallback(async () => {
|
|
3651
|
+
const [text, setText] = React4.useState("");
|
|
3652
|
+
const [isSending, setIsSending] = React4.useState(false);
|
|
3653
|
+
const inputRef = React4.useRef(null);
|
|
3654
|
+
const handleSend = React4.useCallback(async () => {
|
|
3757
3655
|
const trimmed = text.trim();
|
|
3758
3656
|
if (!trimmed || isSending) return;
|
|
3759
3657
|
setIsSending(true);
|
|
@@ -3765,7 +3663,7 @@ function InlineInputBar({
|
|
|
3765
3663
|
setIsSending(false);
|
|
3766
3664
|
}
|
|
3767
3665
|
}, [text, isSending, onSend]);
|
|
3768
|
-
const handleKeyDown =
|
|
3666
|
+
const handleKeyDown = React4.useCallback(
|
|
3769
3667
|
(e) => {
|
|
3770
3668
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
3771
3669
|
e.preventDefault();
|
|
@@ -3774,46 +3672,6 @@ function InlineInputBar({
|
|
|
3774
3672
|
},
|
|
3775
3673
|
[handleSend]
|
|
3776
3674
|
);
|
|
3777
|
-
const handleStopRecording = react.useCallback(async () => {
|
|
3778
|
-
const result = await stopRecording();
|
|
3779
|
-
if (result) {
|
|
3780
|
-
setIsSending(true);
|
|
3781
|
-
try {
|
|
3782
|
-
await onSendAudio(result.blob, result.duration);
|
|
3783
|
-
} finally {
|
|
3784
|
-
setIsSending(false);
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
}, [stopRecording, onSendAudio]);
|
|
3788
|
-
if (isRecording) {
|
|
3789
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.recordingBar, children: [
|
|
3790
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.recordingDot }),
|
|
3791
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles13.recordingLabel, children: [
|
|
3792
|
-
"Recording ",
|
|
3793
|
-
formatDuration3(duration)
|
|
3794
|
-
] }),
|
|
3795
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3796
|
-
"button",
|
|
3797
|
-
{
|
|
3798
|
-
onClick: cancelRecording,
|
|
3799
|
-
style: styles13.recordingCancel,
|
|
3800
|
-
title: "Cancel recording",
|
|
3801
|
-
type: "button",
|
|
3802
|
-
children: "Cancel"
|
|
3803
|
-
}
|
|
3804
|
-
),
|
|
3805
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3806
|
-
"button",
|
|
3807
|
-
{
|
|
3808
|
-
onClick: handleStopRecording,
|
|
3809
|
-
style: styles13.recordingSend,
|
|
3810
|
-
title: "Send voice message",
|
|
3811
|
-
type: "button",
|
|
3812
|
-
children: "Send"
|
|
3813
|
-
}
|
|
3814
|
-
)
|
|
3815
|
-
] });
|
|
3816
|
-
}
|
|
3817
3675
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.inputBar, children: [
|
|
3818
3676
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3819
3677
|
"input",
|
|
@@ -3823,24 +3681,13 @@ function InlineInputBar({
|
|
|
3823
3681
|
value: text,
|
|
3824
3682
|
onChange: (e) => setText(e.target.value),
|
|
3825
3683
|
onKeyDown: handleKeyDown,
|
|
3826
|
-
placeholder
|
|
3684
|
+
placeholder,
|
|
3827
3685
|
disabled: isSending,
|
|
3828
3686
|
style: styles13.input
|
|
3829
3687
|
}
|
|
3830
3688
|
),
|
|
3831
3689
|
unreadCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
|
|
3832
3690
|
isLive && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.liveDot, title: "Live updates active" }),
|
|
3833
|
-
micSupported && !text.trim() && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3834
|
-
"button",
|
|
3835
|
-
{
|
|
3836
|
-
onClick: startRecording,
|
|
3837
|
-
disabled: isSending,
|
|
3838
|
-
style: styles13.micButton,
|
|
3839
|
-
title: "Record voice message",
|
|
3840
|
-
type: "button",
|
|
3841
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(MicIcon, { size: 16, color: "#374151" })
|
|
3842
|
-
}
|
|
3843
|
-
),
|
|
3844
3691
|
text.trim() && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3845
3692
|
"button",
|
|
3846
3693
|
{
|
|
@@ -4018,19 +3865,6 @@ var styles13 = {
|
|
|
4018
3865
|
borderRadius: "50%",
|
|
4019
3866
|
backgroundColor: "#22c55e"
|
|
4020
3867
|
},
|
|
4021
|
-
micButton: {
|
|
4022
|
-
flexShrink: 0,
|
|
4023
|
-
width: "28px",
|
|
4024
|
-
height: "28px",
|
|
4025
|
-
display: "flex",
|
|
4026
|
-
alignItems: "center",
|
|
4027
|
-
justifyContent: "center",
|
|
4028
|
-
fontSize: "13px",
|
|
4029
|
-
backgroundColor: "#f3f4f6",
|
|
4030
|
-
border: "none",
|
|
4031
|
-
borderRadius: "50%",
|
|
4032
|
-
cursor: "pointer"
|
|
4033
|
-
},
|
|
4034
3868
|
sendButton: {
|
|
4035
3869
|
flexShrink: 0,
|
|
4036
3870
|
width: "28px",
|
|
@@ -4058,61 +3892,14 @@ var styles13 = {
|
|
|
4058
3892
|
border: "1px solid #e5e7eb",
|
|
4059
3893
|
borderRadius: "50%",
|
|
4060
3894
|
cursor: "pointer"
|
|
4061
|
-
}
|
|
4062
|
-
// ── Recording mode ──
|
|
4063
|
-
recordingBar: {
|
|
4064
|
-
display: "flex",
|
|
4065
|
-
alignItems: "center",
|
|
4066
|
-
gap: "8px",
|
|
4067
|
-
padding: "6px 10px",
|
|
4068
|
-
backgroundColor: "#fef2f2",
|
|
4069
|
-
border: "1px solid #fecaca",
|
|
4070
|
-
borderRadius: "16px"
|
|
4071
|
-
},
|
|
4072
|
-
recordingDot: {
|
|
4073
|
-
width: "8px",
|
|
4074
|
-
height: "8px",
|
|
4075
|
-
borderRadius: "50%",
|
|
4076
|
-
backgroundColor: "#dc2626",
|
|
4077
|
-
flexShrink: 0,
|
|
4078
|
-
animation: "pulse 1.5s infinite"
|
|
4079
|
-
},
|
|
4080
|
-
recordingLabel: {
|
|
4081
|
-
flex: 1,
|
|
4082
|
-
fontSize: "12px",
|
|
4083
|
-
fontWeight: 500,
|
|
4084
|
-
color: "#dc2626",
|
|
4085
|
-
fontVariantNumeric: "tabular-nums"
|
|
4086
|
-
},
|
|
4087
|
-
recordingCancel: {
|
|
4088
|
-
padding: "4px 10px",
|
|
4089
|
-
fontSize: "11px",
|
|
4090
|
-
color: "#6b7280",
|
|
4091
|
-
backgroundColor: "#ffffff",
|
|
4092
|
-
border: "1px solid #d1d5db",
|
|
4093
|
-
borderRadius: "12px",
|
|
4094
|
-
cursor: "pointer",
|
|
4095
|
-
flexShrink: 0
|
|
4096
|
-
},
|
|
4097
|
-
recordingSend: {
|
|
4098
|
-
padding: "4px 10px",
|
|
4099
|
-
fontSize: "11px",
|
|
4100
|
-
fontWeight: 500,
|
|
4101
|
-
color: "#ffffff",
|
|
4102
|
-
backgroundColor: "#dc2626",
|
|
4103
|
-
border: "none",
|
|
4104
|
-
borderRadius: "12px",
|
|
4105
|
-
cursor: "pointer",
|
|
4106
|
-
flexShrink: 0
|
|
4107
|
-
}
|
|
4108
|
-
};
|
|
3895
|
+
}};
|
|
4109
3896
|
function useConversationList(options) {
|
|
4110
3897
|
const enabled = options?.enabled ?? true;
|
|
4111
3898
|
const { fetchConversationList, config, totalUnread: serverTotalUnread } = useCollab();
|
|
4112
|
-
const [conversations, setConversations] =
|
|
4113
|
-
const [isLoading, setIsLoading] =
|
|
4114
|
-
const [error, setError] =
|
|
4115
|
-
const silentRefresh =
|
|
3899
|
+
const [conversations, setConversations] = React4.useState([]);
|
|
3900
|
+
const [isLoading, setIsLoading] = React4.useState(enabled);
|
|
3901
|
+
const [error, setError] = React4.useState(null);
|
|
3902
|
+
const silentRefresh = React4.useCallback(async () => {
|
|
4116
3903
|
try {
|
|
4117
3904
|
const list = await fetchConversationList();
|
|
4118
3905
|
setConversations(list);
|
|
@@ -4124,7 +3911,7 @@ function useConversationList(options) {
|
|
|
4124
3911
|
});
|
|
4125
3912
|
}
|
|
4126
3913
|
}, [fetchConversationList, config]);
|
|
4127
|
-
const refresh =
|
|
3914
|
+
const refresh = React4.useCallback(async () => {
|
|
4128
3915
|
setIsLoading(true);
|
|
4129
3916
|
setError(null);
|
|
4130
3917
|
try {
|
|
@@ -4142,12 +3929,12 @@ function useConversationList(options) {
|
|
|
4142
3929
|
setIsLoading(false);
|
|
4143
3930
|
}
|
|
4144
3931
|
}, [fetchConversationList, config]);
|
|
4145
|
-
|
|
3932
|
+
React4.useEffect(() => {
|
|
4146
3933
|
if (!enabled) return;
|
|
4147
3934
|
refresh();
|
|
4148
3935
|
}, [enabled, refresh]);
|
|
4149
|
-
const hasHandledInitialUnreadRef =
|
|
4150
|
-
|
|
3936
|
+
const hasHandledInitialUnreadRef = React4.useRef(false);
|
|
3937
|
+
React4.useEffect(() => {
|
|
4151
3938
|
if (!enabled) return;
|
|
4152
3939
|
if (!hasHandledInitialUnreadRef.current) {
|
|
4153
3940
|
hasHandledInitialUnreadRef.current = true;
|
|
@@ -4155,7 +3942,7 @@ function useConversationList(options) {
|
|
|
4155
3942
|
}
|
|
4156
3943
|
silentRefresh();
|
|
4157
3944
|
}, [serverTotalUnread, enabled]);
|
|
4158
|
-
const filter =
|
|
3945
|
+
const filter = React4.useCallback(
|
|
4159
3946
|
(query) => {
|
|
4160
3947
|
const q = query.trim().toLowerCase();
|
|
4161
3948
|
if (!q) return conversations;
|
|
@@ -4220,7 +4007,7 @@ function ConversationListItem({
|
|
|
4220
4007
|
...styles14.time,
|
|
4221
4008
|
...hasUnread ? styles14.timeUnread : {}
|
|
4222
4009
|
},
|
|
4223
|
-
children:
|
|
4010
|
+
children: formatInboxTimestamp(item.lastActivityAt)
|
|
4224
4011
|
}
|
|
4225
4012
|
)
|
|
4226
4013
|
] }),
|
|
@@ -4263,23 +4050,6 @@ function renderLastMessagePreview(message) {
|
|
|
4263
4050
|
}
|
|
4264
4051
|
}
|
|
4265
4052
|
}
|
|
4266
|
-
function formatRelativeTime(iso) {
|
|
4267
|
-
try {
|
|
4268
|
-
const date = new Date(iso);
|
|
4269
|
-
const now = /* @__PURE__ */ new Date();
|
|
4270
|
-
const diffMs = now.getTime() - date.getTime();
|
|
4271
|
-
const diffMin = Math.floor(diffMs / 6e4);
|
|
4272
|
-
const diffHr = Math.floor(diffMs / 36e5);
|
|
4273
|
-
const diffDay = Math.floor(diffMs / 864e5);
|
|
4274
|
-
if (diffMin < 1) return "now";
|
|
4275
|
-
if (diffMin < 60) return `${diffMin}m`;
|
|
4276
|
-
if (diffHr < 24) return `${diffHr}h`;
|
|
4277
|
-
if (diffDay < 7) return `${diffDay}d`;
|
|
4278
|
-
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
|
4279
|
-
} catch {
|
|
4280
|
-
return "";
|
|
4281
|
-
}
|
|
4282
|
-
}
|
|
4283
4053
|
var styles14 = {
|
|
4284
4054
|
container: {
|
|
4285
4055
|
display: "flex",
|
|
@@ -4409,9 +4179,9 @@ function ConversationList({
|
|
|
4409
4179
|
title = "Conversations",
|
|
4410
4180
|
className
|
|
4411
4181
|
}) {
|
|
4412
|
-
const [query, setQuery] =
|
|
4413
|
-
const [showUnreadOnly, setShowUnreadOnly] =
|
|
4414
|
-
const filtered =
|
|
4182
|
+
const [query, setQuery] = React4.useState("");
|
|
4183
|
+
const [showUnreadOnly, setShowUnreadOnly] = React4.useState(false);
|
|
4184
|
+
const filtered = React4.useMemo(() => {
|
|
4415
4185
|
let result = conversations;
|
|
4416
4186
|
if (showUnreadOnly) {
|
|
4417
4187
|
result = result.filter((c) => c.unreadCount > 0);
|
|
@@ -4588,7 +4358,7 @@ function CollabInbox({
|
|
|
4588
4358
|
style
|
|
4589
4359
|
}) {
|
|
4590
4360
|
const { conversations, isLoading, error } = useConversationList();
|
|
4591
|
-
const [selectedId, setSelectedId] =
|
|
4361
|
+
const [selectedId, setSelectedId] = React4.useState(initialConversationId ?? null);
|
|
4592
4362
|
const selected = conversations.find((c) => c.id === selectedId) || null;
|
|
4593
4363
|
const handleSelect = (item) => {
|
|
4594
4364
|
setSelectedId(item.id);
|
|
@@ -4690,12 +4460,12 @@ function useMessages({
|
|
|
4690
4460
|
initialLimit = MESSAGES_PAGE_SIZE
|
|
4691
4461
|
}) {
|
|
4692
4462
|
const { fetchMessages, config } = useCollab();
|
|
4693
|
-
const [messages, setMessages] =
|
|
4694
|
-
const [isLoading, setIsLoading] =
|
|
4695
|
-
const [hasMore, setHasMore] =
|
|
4696
|
-
const [error, setError] =
|
|
4697
|
-
const loadedRef =
|
|
4698
|
-
|
|
4463
|
+
const [messages, setMessages] = React4.useState([]);
|
|
4464
|
+
const [isLoading, setIsLoading] = React4.useState(false);
|
|
4465
|
+
const [hasMore, setHasMore] = React4.useState(true);
|
|
4466
|
+
const [error, setError] = React4.useState(null);
|
|
4467
|
+
const loadedRef = React4.useRef(null);
|
|
4468
|
+
React4.useEffect(() => {
|
|
4699
4469
|
if (!conversationId) {
|
|
4700
4470
|
setMessages([]);
|
|
4701
4471
|
setHasMore(false);
|
|
@@ -4731,7 +4501,7 @@ function useMessages({
|
|
|
4731
4501
|
cancelled = true;
|
|
4732
4502
|
};
|
|
4733
4503
|
}, [conversationId, initialLimit, fetchMessages, config]);
|
|
4734
|
-
const loadMore =
|
|
4504
|
+
const loadMore = React4.useCallback(async () => {
|
|
4735
4505
|
if (!conversationId || !hasMore || isLoading) return;
|
|
4736
4506
|
const oldestId = messages[0]?.id;
|
|
4737
4507
|
setIsLoading(true);
|
|
@@ -4752,17 +4522,17 @@ function useMessages({
|
|
|
4752
4522
|
setIsLoading(false);
|
|
4753
4523
|
}
|
|
4754
4524
|
}, [conversationId, hasMore, isLoading, messages, initialLimit, fetchMessages, config]);
|
|
4755
|
-
const addMessage =
|
|
4525
|
+
const addMessage = React4.useCallback((message) => {
|
|
4756
4526
|
setMessages((prev) => [...prev, message]);
|
|
4757
4527
|
}, []);
|
|
4758
|
-
const updateMessageReadBy =
|
|
4528
|
+
const updateMessageReadBy = React4.useCallback((messageId, userId) => {
|
|
4759
4529
|
setMessages(
|
|
4760
4530
|
(prev) => prev.map(
|
|
4761
4531
|
(msg) => msg.id === messageId && !(msg.readBy ?? []).includes(userId) ? { ...msg, readBy: [...msg.readBy ?? [], userId] } : msg
|
|
4762
4532
|
)
|
|
4763
4533
|
);
|
|
4764
4534
|
}, []);
|
|
4765
|
-
const reset =
|
|
4535
|
+
const reset = React4.useCallback(() => {
|
|
4766
4536
|
setMessages([]);
|
|
4767
4537
|
setHasMore(true);
|
|
4768
4538
|
setError(null);
|
|
@@ -4779,21 +4549,122 @@ function useMessages({
|
|
|
4779
4549
|
reset
|
|
4780
4550
|
};
|
|
4781
4551
|
}
|
|
4552
|
+
function useAudioRecorder() {
|
|
4553
|
+
const [isRecording, setIsRecording] = React4.useState(false);
|
|
4554
|
+
const [duration, setDuration] = React4.useState(0);
|
|
4555
|
+
const [error, setError] = React4.useState(null);
|
|
4556
|
+
const mediaRecorderRef = React4.useRef(null);
|
|
4557
|
+
const chunksRef = React4.useRef([]);
|
|
4558
|
+
const streamRef = React4.useRef(null);
|
|
4559
|
+
const timerRef = React4.useRef(null);
|
|
4560
|
+
const startTimeRef = React4.useRef(0);
|
|
4561
|
+
const resolveRef = React4.useRef(null);
|
|
4562
|
+
const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
|
|
4563
|
+
const cleanup = React4.useCallback(() => {
|
|
4564
|
+
if (timerRef.current) {
|
|
4565
|
+
clearInterval(timerRef.current);
|
|
4566
|
+
timerRef.current = null;
|
|
4567
|
+
}
|
|
4568
|
+
if (streamRef.current) {
|
|
4569
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
4570
|
+
streamRef.current = null;
|
|
4571
|
+
}
|
|
4572
|
+
mediaRecorderRef.current = null;
|
|
4573
|
+
chunksRef.current = [];
|
|
4574
|
+
setIsRecording(false);
|
|
4575
|
+
setDuration(0);
|
|
4576
|
+
}, []);
|
|
4577
|
+
const start = React4.useCallback(async () => {
|
|
4578
|
+
if (!isSupported) {
|
|
4579
|
+
setError("Audio recording is not supported in this browser");
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4582
|
+
try {
|
|
4583
|
+
setError(null);
|
|
4584
|
+
chunksRef.current = [];
|
|
4585
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
4586
|
+
streamRef.current = stream;
|
|
4587
|
+
const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
|
|
4588
|
+
const recorder = new MediaRecorder(stream, { mimeType });
|
|
4589
|
+
mediaRecorderRef.current = recorder;
|
|
4590
|
+
recorder.ondataavailable = (event) => {
|
|
4591
|
+
if (event.data.size > 0) {
|
|
4592
|
+
chunksRef.current.push(event.data);
|
|
4593
|
+
}
|
|
4594
|
+
};
|
|
4595
|
+
recorder.onerror = () => {
|
|
4596
|
+
setError("Recording failed");
|
|
4597
|
+
cleanup();
|
|
4598
|
+
resolveRef.current?.(null);
|
|
4599
|
+
resolveRef.current = null;
|
|
4600
|
+
};
|
|
4601
|
+
recorder.onstop = () => {
|
|
4602
|
+
const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
4603
|
+
const blob = new Blob(chunksRef.current, { type: mimeType });
|
|
4604
|
+
cleanup();
|
|
4605
|
+
resolveRef.current?.({ blob, duration: finalDuration });
|
|
4606
|
+
resolveRef.current = null;
|
|
4607
|
+
};
|
|
4608
|
+
recorder.start(250);
|
|
4609
|
+
startTimeRef.current = Date.now();
|
|
4610
|
+
setIsRecording(true);
|
|
4611
|
+
timerRef.current = setInterval(() => {
|
|
4612
|
+
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
4613
|
+
setDuration(elapsed);
|
|
4614
|
+
}, 1e3);
|
|
4615
|
+
} catch (err) {
|
|
4616
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
4617
|
+
setError("Microphone access denied. Please allow microphone permissions.");
|
|
4618
|
+
} else {
|
|
4619
|
+
setError("Failed to start recording");
|
|
4620
|
+
}
|
|
4621
|
+
cleanup();
|
|
4622
|
+
}
|
|
4623
|
+
}, [isSupported, cleanup]);
|
|
4624
|
+
const stop = React4.useCallback(async () => {
|
|
4625
|
+
return new Promise((resolve) => {
|
|
4626
|
+
if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
|
|
4627
|
+
resolve(null);
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
resolveRef.current = resolve;
|
|
4631
|
+
mediaRecorderRef.current.stop();
|
|
4632
|
+
});
|
|
4633
|
+
}, []);
|
|
4634
|
+
const cancel = React4.useCallback(() => {
|
|
4635
|
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
4636
|
+
mediaRecorderRef.current.onstop = null;
|
|
4637
|
+
mediaRecorderRef.current.stop();
|
|
4638
|
+
}
|
|
4639
|
+
cleanup();
|
|
4640
|
+
resolveRef.current?.(null);
|
|
4641
|
+
resolveRef.current = null;
|
|
4642
|
+
}, [cleanup]);
|
|
4643
|
+
return {
|
|
4644
|
+
isRecording,
|
|
4645
|
+
duration,
|
|
4646
|
+
start,
|
|
4647
|
+
stop,
|
|
4648
|
+
cancel,
|
|
4649
|
+
isSupported,
|
|
4650
|
+
error
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4782
4653
|
function useUnreadCount() {
|
|
4783
4654
|
const { socket, totalUnread } = useCollab();
|
|
4784
|
-
const [counts, setCounts] =
|
|
4785
|
-
|
|
4655
|
+
const [counts, setCounts] = React4.useState({});
|
|
4656
|
+
React4.useEffect(() => {
|
|
4786
4657
|
socket.onUnreadCountUpdate((serverCounts) => {
|
|
4787
4658
|
setCounts(serverCounts);
|
|
4788
4659
|
});
|
|
4789
4660
|
}, [socket]);
|
|
4790
|
-
const getCountForConversation =
|
|
4661
|
+
const getCountForConversation = React4.useCallback(
|
|
4791
4662
|
(conversationId) => {
|
|
4792
4663
|
return counts[conversationId] ?? 0;
|
|
4793
4664
|
},
|
|
4794
4665
|
[counts]
|
|
4795
4666
|
);
|
|
4796
|
-
const clearForConversation =
|
|
4667
|
+
const clearForConversation = React4.useCallback((conversationId) => {
|
|
4797
4668
|
setCounts((prev) => {
|
|
4798
4669
|
const next = { ...prev };
|
|
4799
4670
|
delete next[conversationId];
|
|
@@ -4811,10 +4682,10 @@ function usePinnedMessages({
|
|
|
4811
4682
|
initial = []
|
|
4812
4683
|
}) {
|
|
4813
4684
|
const { pinMessage, unpinMessage, fetchPinnedMessages, config } = useCollab();
|
|
4814
|
-
const [pinnedMessages, setPinnedMessages] =
|
|
4815
|
-
const [isLoading, setIsLoading] =
|
|
4816
|
-
const [error, setError] =
|
|
4817
|
-
const refresh =
|
|
4685
|
+
const [pinnedMessages, setPinnedMessages] = React4.useState(initial);
|
|
4686
|
+
const [isLoading, setIsLoading] = React4.useState(false);
|
|
4687
|
+
const [error, setError] = React4.useState(null);
|
|
4688
|
+
const refresh = React4.useCallback(async () => {
|
|
4818
4689
|
if (!conversationId) return;
|
|
4819
4690
|
setIsLoading(true);
|
|
4820
4691
|
setError(null);
|
|
@@ -4828,12 +4699,12 @@ function usePinnedMessages({
|
|
|
4828
4699
|
setIsLoading(false);
|
|
4829
4700
|
}
|
|
4830
4701
|
}, [conversationId, fetchPinnedMessages]);
|
|
4831
|
-
|
|
4702
|
+
React4.useEffect(() => {
|
|
4832
4703
|
if (conversationId && initial.length === 0) {
|
|
4833
4704
|
refresh();
|
|
4834
4705
|
}
|
|
4835
4706
|
}, [conversationId]);
|
|
4836
|
-
const pin =
|
|
4707
|
+
const pin = React4.useCallback(
|
|
4837
4708
|
async (messageId) => {
|
|
4838
4709
|
if (!conversationId) return;
|
|
4839
4710
|
setError(null);
|
|
@@ -4851,7 +4722,7 @@ function usePinnedMessages({
|
|
|
4851
4722
|
},
|
|
4852
4723
|
[conversationId, pinMessage, config]
|
|
4853
4724
|
);
|
|
4854
|
-
const unpin =
|
|
4725
|
+
const unpin = React4.useCallback(
|
|
4855
4726
|
async (messageId) => {
|
|
4856
4727
|
if (!conversationId) return;
|
|
4857
4728
|
setError(null);
|
|
@@ -4869,13 +4740,13 @@ function usePinnedMessages({
|
|
|
4869
4740
|
},
|
|
4870
4741
|
[conversationId, unpinMessage, config]
|
|
4871
4742
|
);
|
|
4872
|
-
const handlePinned =
|
|
4743
|
+
const handlePinned = React4.useCallback((message) => {
|
|
4873
4744
|
setPinnedMessages((prev) => {
|
|
4874
4745
|
if (prev.some((m) => m.id === message.id)) return prev;
|
|
4875
4746
|
return [...prev, message];
|
|
4876
4747
|
});
|
|
4877
4748
|
}, []);
|
|
4878
|
-
const handleUnpinned =
|
|
4749
|
+
const handleUnpinned = React4.useCallback((messageId) => {
|
|
4879
4750
|
setPinnedMessages((prev) => prev.filter((m) => m.id !== messageId));
|
|
4880
4751
|
}, []);
|
|
4881
4752
|
return {
|