@natoe/colab 0.1.0

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.js ADDED
@@ -0,0 +1,4932 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var phoenix = require('phoenix');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/components/CollabPanel.tsx
8
+
9
+ // src/core/constants.ts
10
+ var EVENTS = {
11
+ MESSAGE_NEW: "message:new",
12
+ MESSAGE_READ: "message:read",
13
+ MESSAGE_PINNED: "message:pinned",
14
+ MESSAGE_UNPINNED: "message:unpinned",
15
+ USER_TYPING: "user:typing",
16
+ USER_JOINED: "user:joined",
17
+ USER_LEFT: "user:left",
18
+ CHANNEL_UPDATED: "channel:updated",
19
+ CHANNEL_DELETED: "channel:deleted",
20
+ PRESENCE_STATE: "presence_state",
21
+ PRESENCE_DIFF: "presence_diff"
22
+ };
23
+ var MAX_PINNED_MESSAGES = 3;
24
+ var MESSAGE_TYPES = {
25
+ TEXT: "text",
26
+ AUDIO: "audio",
27
+ IMAGE: "image",
28
+ FILE: "file",
29
+ SYSTEM: "system",
30
+ DEEP_LINK: "deep_link"
31
+ };
32
+ var DEEP_LINK_PREFIX = "natoe://";
33
+ var TYPING_DEBOUNCE_MS = 2e3;
34
+ var MESSAGES_PAGE_SIZE = 30;
35
+ var AUDIO_MIME_TYPE = "audio/webm;codecs=opus";
36
+ var SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
37
+ var MAX_FILE_SIZE = 20 * 1024 * 1024;
38
+
39
+ // src/core/caseConvert.ts
40
+ function snakeToCamel(input) {
41
+ if (Array.isArray(input)) {
42
+ return input.map((item) => snakeToCamel(item));
43
+ }
44
+ if (input !== null && typeof input === "object") {
45
+ const source = input;
46
+ const out = {};
47
+ for (const key of Object.keys(source)) {
48
+ const camelKey = toCamelKey(key);
49
+ out[camelKey] = snakeToCamel(source[key]);
50
+ }
51
+ return out;
52
+ }
53
+ return input;
54
+ }
55
+ function toCamelKey(key) {
56
+ if (!key.includes("_")) return key;
57
+ return key.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
58
+ }
59
+
60
+ // src/core/socket.ts
61
+ var CollabSocket = class {
62
+ constructor() {
63
+ this.socket = null;
64
+ this.channels = /* @__PURE__ */ new Map();
65
+ this.presences = /* @__PURE__ */ new Map();
66
+ this.userChannel = null;
67
+ this.config = null;
68
+ this.onUnreadUpdate = null;
69
+ }
70
+ /** Connect to Phoenix WebSocket server. Idempotent — safe to call when
71
+ * the token becomes available later (e.g. after async login) without
72
+ * stacking up multiple open sockets. */
73
+ connect(config) {
74
+ this.config = config;
75
+ if (!config.getToken()) {
76
+ return;
77
+ }
78
+ if (this.socket) {
79
+ if (this.socket.isConnected()) return;
80
+ try {
81
+ this.socket.disconnect();
82
+ } catch {
83
+ }
84
+ this.channels.clear();
85
+ this.presences.clear();
86
+ this.userChannel = null;
87
+ }
88
+ this.socket = new phoenix.Socket(config.socketUrl, {
89
+ params: () => ({ token: config.getToken() })
90
+ });
91
+ this.socket.onError((err) => {
92
+ const closeEvent = err;
93
+ config.onError?.({
94
+ code: "SOCKET_ERROR",
95
+ message: "WebSocket connection error",
96
+ details: closeEvent ? {
97
+ type: closeEvent.type,
98
+ code: closeEvent.code,
99
+ reason: closeEvent.reason,
100
+ wasClean: closeEvent.wasClean
101
+ } : err
102
+ });
103
+ });
104
+ this.socket.onClose((event) => {
105
+ config.onError?.({
106
+ code: "SOCKET_CLOSED",
107
+ message: `Socket closed (code ${event?.code ?? "unknown"})`,
108
+ details: event ? { code: event.code, reason: event.reason, wasClean: event.wasClean } : void 0
109
+ });
110
+ });
111
+ this.socket.connect();
112
+ this.joinUserChannel();
113
+ }
114
+ /** Subscribe to user-level events (unread counts, notifications) */
115
+ joinUserChannel() {
116
+ if (!this.socket || !this.config) return;
117
+ this.userChannel = this.socket.channel("user_notifications", {});
118
+ this.userChannel.on("unread_update", (payload) => {
119
+ this.onUnreadUpdate?.(payload);
120
+ });
121
+ this.userChannel.join().receive("ok", () => {
122
+ }).receive("error", (reason) => {
123
+ this.config?.onError?.({
124
+ code: "USER_CHANNEL_ERROR",
125
+ message: "Failed to join user channel",
126
+ details: reason
127
+ });
128
+ });
129
+ }
130
+ /** Register callback for unread count changes */
131
+ onUnreadCountUpdate(callback) {
132
+ this.onUnreadUpdate = callback;
133
+ }
134
+ /** Join a conversation channel and subscribe to events */
135
+ joinConversation(conversationId, callbacks) {
136
+ if (!this.socket) return null;
137
+ if (this.channels.has(conversationId)) {
138
+ return this.channels.get(conversationId);
139
+ }
140
+ const channel = this.socket.channel(`conversation:${conversationId}`, {});
141
+ if (callbacks.onMessage) {
142
+ channel.on(EVENTS.MESSAGE_NEW, (payload) => {
143
+ callbacks.onMessage(snakeToCamel(payload));
144
+ });
145
+ }
146
+ if (callbacks.onTyping) {
147
+ channel.on(EVENTS.USER_TYPING, (payload) => {
148
+ callbacks.onTyping(snakeToCamel(payload));
149
+ });
150
+ }
151
+ if (callbacks.onUserJoined) {
152
+ channel.on(EVENTS.USER_JOINED, (payload) => {
153
+ callbacks.onUserJoined(snakeToCamel(payload));
154
+ });
155
+ }
156
+ if (callbacks.onUserLeft) {
157
+ channel.on(EVENTS.USER_LEFT, (payload) => {
158
+ callbacks.onUserLeft(snakeToCamel(payload));
159
+ });
160
+ }
161
+ if (callbacks.onChannelUpdated) {
162
+ channel.on(EVENTS.CHANNEL_UPDATED, (payload) => {
163
+ callbacks.onChannelUpdated(snakeToCamel(payload));
164
+ });
165
+ }
166
+ if (callbacks.onChannelDeleted) {
167
+ channel.on(EVENTS.CHANNEL_DELETED, () => {
168
+ callbacks.onChannelDeleted();
169
+ });
170
+ }
171
+ if (callbacks.onMessageRead) {
172
+ channel.on(EVENTS.MESSAGE_READ, (payload) => {
173
+ callbacks.onMessageRead(snakeToCamel(payload));
174
+ });
175
+ }
176
+ if (callbacks.onMessagePinned) {
177
+ channel.on(EVENTS.MESSAGE_PINNED, (payload) => {
178
+ callbacks.onMessagePinned(snakeToCamel(payload));
179
+ });
180
+ }
181
+ if (callbacks.onMessageUnpinned) {
182
+ channel.on(EVENTS.MESSAGE_UNPINNED, (payload) => {
183
+ callbacks.onMessageUnpinned(snakeToCamel(payload));
184
+ });
185
+ }
186
+ if (callbacks.onPresence) {
187
+ const presence = new phoenix.Presence(channel);
188
+ presence.onSync(() => {
189
+ const online = {};
190
+ presence.list((id) => {
191
+ online[id] = { userId: id, isOnline: true };
192
+ });
193
+ callbacks.onPresence(online);
194
+ });
195
+ this.presences.set(conversationId, presence);
196
+ }
197
+ channel.join().receive("ok", () => {
198
+ }).receive("error", (reason) => {
199
+ this.config?.onError?.({
200
+ code: "CHANNEL_JOIN_ERROR",
201
+ message: `Failed to join conversation ${conversationId}`,
202
+ details: reason
203
+ });
204
+ });
205
+ this.channels.set(conversationId, channel);
206
+ return channel;
207
+ }
208
+ /** Leave a conversation channel */
209
+ leaveConversation(conversationId) {
210
+ const channel = this.channels.get(conversationId);
211
+ if (channel) {
212
+ channel.leave();
213
+ this.channels.delete(conversationId);
214
+ this.presences.delete(conversationId);
215
+ }
216
+ }
217
+ /** Send a message to a conversation.
218
+ *
219
+ * Phoenix buffers pushes fired while a channel is still joining and
220
+ * replays them on successful join. The default per-push timeout is 10s,
221
+ * which is tight on slow dev backends — we bump to 30s so a late join
222
+ * doesn't surface as a "timed out" error to a message that ultimately
223
+ * persists. The caller still gets 'ok' once the server acks.
224
+ */
225
+ sendMessage(conversationId, payload) {
226
+ return new Promise((resolve, reject) => {
227
+ const channel = this.channels.get(conversationId);
228
+ if (!channel) {
229
+ reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
230
+ return;
231
+ }
232
+ if (!this.socket?.isConnected()) {
233
+ reject({
234
+ code: "SOCKET_DISCONNECTED",
235
+ message: "Cannot send \u2014 chat server not connected"
236
+ });
237
+ return;
238
+ }
239
+ channel.push(EVENTS.MESSAGE_NEW, payload, 3e4).receive("ok", (response) => resolve(response)).receive(
240
+ "error",
241
+ (reason) => reject({ code: "SEND_ERROR", message: "Failed to send message", details: reason })
242
+ ).receive(
243
+ "timeout",
244
+ () => reject({ code: "TIMEOUT", message: "Message send timed out" })
245
+ );
246
+ });
247
+ }
248
+ /** Broadcast typing indicator */
249
+ sendTyping(conversationId, isTyping) {
250
+ const channel = this.channels.get(conversationId);
251
+ channel?.push(EVENTS.USER_TYPING, {
252
+ userId: this.config?.userId,
253
+ userName: this.config?.userName,
254
+ isTyping
255
+ });
256
+ }
257
+ /** Mark messages as read */
258
+ markAsRead(conversationId, messageId) {
259
+ const channel = this.channels.get(conversationId);
260
+ channel?.push(EVENTS.MESSAGE_READ, {
261
+ messageId,
262
+ userId: this.config?.userId
263
+ });
264
+ }
265
+ // ── Channel Management ──
266
+ /** Add a user to the conversation */
267
+ inviteUser(conversationId, payload) {
268
+ return this.channelPush(conversationId, "invite_user", payload);
269
+ }
270
+ /** Remove a user from the conversation */
271
+ removeUser(conversationId, userId) {
272
+ return this.channelPush(conversationId, "remove_user", { userId });
273
+ }
274
+ /** Update channel name/picture */
275
+ updateChannel(conversationId, payload) {
276
+ return this.channelPush(conversationId, "update_channel", payload);
277
+ }
278
+ /** Delete channel (admin only) */
279
+ deleteChannel(conversationId) {
280
+ return this.channelPush(conversationId, "delete_channel", {});
281
+ }
282
+ /** Pin a message in the conversation (max MAX_PINNED_MESSAGES) */
283
+ pinMessage(conversationId, messageId) {
284
+ return this.channelPush(conversationId, "pin_message", { messageId });
285
+ }
286
+ /** Unpin a message in the conversation */
287
+ unpinMessage(conversationId, messageId) {
288
+ return this.channelPush(conversationId, "unpin_message", { messageId });
289
+ }
290
+ /** Generic push with promise wrapper */
291
+ channelPush(conversationId, event, payload) {
292
+ return new Promise((resolve, reject) => {
293
+ const channel = this.channels.get(conversationId);
294
+ if (!channel) {
295
+ reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
296
+ return;
297
+ }
298
+ channel.push(event, payload).receive("ok", (response) => resolve(response)).receive(
299
+ "error",
300
+ (reason) => reject({ code: "CHANNEL_ERROR", message: `Failed: ${event}`, details: reason })
301
+ );
302
+ });
303
+ }
304
+ /** Check if connected */
305
+ isConnected() {
306
+ return this.socket?.isConnected() ?? false;
307
+ }
308
+ /** Disconnect socket and leave all channels */
309
+ disconnect() {
310
+ this.channels.forEach((channel) => channel.leave());
311
+ this.channels.clear();
312
+ this.presences.clear();
313
+ this.userChannel?.leave();
314
+ this.socket?.disconnect();
315
+ this.socket = null;
316
+ this.config = null;
317
+ }
318
+ };
319
+ var CollabContext = react.createContext(null);
320
+ function useCollab() {
321
+ const context = react.useContext(CollabContext);
322
+ if (!context) {
323
+ throw new Error("useCollab must be used within a <CollabProvider>");
324
+ }
325
+ return context;
326
+ }
327
+ function CollabProvider({ config, apiBaseUrl, children }) {
328
+ const [socket, setSocket] = react.useState(() => {
329
+ if (typeof window === "undefined") return null;
330
+ return new CollabSocket();
331
+ });
332
+ const [unreadCounts, setUnreadCounts] = react.useState({});
333
+ const pendingOrderIds = react.useRef(/* @__PURE__ */ new Set());
334
+ const pendingResolvers = react.useRef(/* @__PURE__ */ new Map());
335
+ const previewCache = react.useRef(/* @__PURE__ */ new Map());
336
+ const batchScheduled = react.useRef(false);
337
+ react.useEffect(() => {
338
+ if (typeof window === "undefined") return;
339
+ let s = socket;
340
+ if (!s) {
341
+ s = new CollabSocket();
342
+ setSocket(s);
343
+ }
344
+ s.onUnreadCountUpdate((counts) => {
345
+ setUnreadCounts(counts);
346
+ previewCache.current.clear();
347
+ });
348
+ if (!s.isConnected()) {
349
+ s.connect(config);
350
+ }
351
+ }, [config.socketUrl, config.userId]);
352
+ const authHeaders = react.useCallback(
353
+ () => ({
354
+ Authorization: `Bearer ${config.getToken()}`,
355
+ "Content-Type": "application/json"
356
+ }),
357
+ [config]
358
+ );
359
+ const flushPreviewBatch = react.useCallback(async () => {
360
+ const orderIds = Array.from(pendingOrderIds.current);
361
+ const resolvers = new Map(pendingResolvers.current);
362
+ pendingOrderIds.current.clear();
363
+ pendingResolvers.current.clear();
364
+ batchScheduled.current = false;
365
+ if (orderIds.length === 0) return;
366
+ try {
367
+ const query = orderIds.map((id) => `order_ids[]=${encodeURIComponent(id)}`).join("&");
368
+ const response = await fetch(`${apiBaseUrl}/api/natoe-colab/conversations/previews?${query}`, {
369
+ headers: authHeaders()
370
+ });
371
+ if (!response.ok) throw new Error(`Preview batch failed: ${response.status}`);
372
+ const data = snakeToCamel(await response.json());
373
+ orderIds.forEach((orderId) => {
374
+ const preview = data.previews[orderId] ?? null;
375
+ previewCache.current.set(orderId, preview);
376
+ resolvers.get(orderId)?.forEach((resolve) => resolve(preview));
377
+ });
378
+ } catch (error) {
379
+ config.onError?.({
380
+ code: "PREVIEW_BATCH_ERROR",
381
+ message: "Failed to fetch conversation previews",
382
+ details: error
383
+ });
384
+ orderIds.forEach((orderId) => {
385
+ resolvers.get(orderId)?.forEach((resolve) => resolve(null));
386
+ });
387
+ }
388
+ }, [apiBaseUrl, authHeaders, config]);
389
+ const requestPreview = react.useCallback(
390
+ (orderId) => {
391
+ if (previewCache.current.has(orderId)) {
392
+ return Promise.resolve(previewCache.current.get(orderId) ?? null);
393
+ }
394
+ return new Promise((resolve) => {
395
+ pendingOrderIds.current.add(orderId);
396
+ const existing = pendingResolvers.current.get(orderId) || [];
397
+ existing.push(resolve);
398
+ pendingResolvers.current.set(orderId, existing);
399
+ if (!batchScheduled.current) {
400
+ batchScheduled.current = true;
401
+ queueMicrotask(flushPreviewBatch);
402
+ }
403
+ });
404
+ },
405
+ [flushPreviewBatch]
406
+ );
407
+ const invalidatePreview = react.useCallback((orderId) => {
408
+ previewCache.current.delete(orderId);
409
+ }, []);
410
+ const fetchMessages = react.useCallback(
411
+ async (conversationId, options = {}) => {
412
+ const limit = options.limit ?? MESSAGES_PAGE_SIZE;
413
+ const params = new URLSearchParams({ limit: String(limit) });
414
+ if (options.before) params.set("before", options.before);
415
+ const response = await fetch(
416
+ `${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/messages?${params.toString()}`,
417
+ { headers: authHeaders() }
418
+ );
419
+ if (!response.ok) {
420
+ throw { code: "FETCH_ERROR", message: "Failed to fetch messages", status: response.status };
421
+ }
422
+ return snakeToCamel(await response.json());
423
+ },
424
+ [apiBaseUrl, authHeaders]
425
+ );
426
+ const createConversationWithMessage = react.useCallback(
427
+ async (orderId, message, options) => {
428
+ if (!orderId || !orderId.trim()) {
429
+ throw {
430
+ code: "INVALID_ORDER_ID",
431
+ message: "Cannot create conversation: orderId is empty. Host app passed an order with no orderId/mainOrderId set."
432
+ };
433
+ }
434
+ const response = await fetch(`${apiBaseUrl}/api/natoe-colab/orders/${orderId}/messages`, {
435
+ method: "POST",
436
+ headers: authHeaders(),
437
+ body: JSON.stringify({
438
+ message,
439
+ conversation: {
440
+ name: options.name,
441
+ participant_ids: options.participantIds ?? []
442
+ }
443
+ })
444
+ });
445
+ if (!response.ok) {
446
+ const error = await response.json().catch(() => ({}));
447
+ throw { code: "CREATE_ERROR", message: "Failed to create conversation", details: error };
448
+ }
449
+ const data = snakeToCamel(await response.json());
450
+ previewCache.current.delete(orderId);
451
+ return data;
452
+ },
453
+ [apiBaseUrl, authHeaders]
454
+ );
455
+ const addParticipant = react.useCallback(
456
+ async (conversationId, userId) => {
457
+ const response = await fetch(
458
+ `${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/participants`,
459
+ {
460
+ method: "POST",
461
+ headers: authHeaders(),
462
+ body: JSON.stringify({ user_id: userId })
463
+ }
464
+ );
465
+ if (!response.ok) {
466
+ throw { code: "ADD_PARTICIPANT_ERROR", message: "Failed to add participant" };
467
+ }
468
+ },
469
+ [apiBaseUrl, authHeaders]
470
+ );
471
+ const removeParticipant = react.useCallback(
472
+ async (conversationId, userId) => {
473
+ const response = await fetch(
474
+ `${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/participants/${userId}`,
475
+ {
476
+ method: "DELETE",
477
+ headers: authHeaders()
478
+ }
479
+ );
480
+ if (!response.ok) {
481
+ throw { code: "REMOVE_PARTICIPANT_ERROR", message: "Failed to remove participant" };
482
+ }
483
+ },
484
+ [apiBaseUrl, authHeaders]
485
+ );
486
+ const fetchConversationList = react.useCallback(async () => {
487
+ const response = await fetch(`${apiBaseUrl}/api/natoe-colab/conversations`, {
488
+ headers: authHeaders()
489
+ });
490
+ if (!response.ok) {
491
+ throw { code: "LIST_ERROR", message: "Failed to fetch conversation list" };
492
+ }
493
+ return snakeToCamel(await response.json());
494
+ }, [apiBaseUrl, authHeaders]);
495
+ const pinMessage = react.useCallback(
496
+ async (conversationId, messageId) => {
497
+ const response = await fetch(
498
+ `${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/messages/${messageId}/pin`,
499
+ {
500
+ method: "POST",
501
+ headers: authHeaders()
502
+ }
503
+ );
504
+ if (!response.ok) {
505
+ const error = await response.json().catch(() => ({}));
506
+ throw { code: "PIN_ERROR", message: "Failed to pin message", details: error };
507
+ }
508
+ },
509
+ [apiBaseUrl, authHeaders]
510
+ );
511
+ const unpinMessage = react.useCallback(
512
+ async (conversationId, messageId) => {
513
+ const response = await fetch(
514
+ `${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/messages/${messageId}/pin`,
515
+ {
516
+ method: "DELETE",
517
+ headers: authHeaders()
518
+ }
519
+ );
520
+ if (!response.ok) {
521
+ throw { code: "UNPIN_ERROR", message: "Failed to unpin message" };
522
+ }
523
+ },
524
+ [apiBaseUrl, authHeaders]
525
+ );
526
+ const fetchPinnedMessages = react.useCallback(
527
+ async (conversationId) => {
528
+ const response = await fetch(
529
+ `${apiBaseUrl}/api/natoe-colab/conversations/${conversationId}/pinned`,
530
+ { headers: authHeaders() }
531
+ );
532
+ if (!response.ok) {
533
+ throw { code: "FETCH_PINNED_ERROR", message: "Failed to fetch pinned messages" };
534
+ }
535
+ return snakeToCamel(await response.json());
536
+ },
537
+ [apiBaseUrl, authHeaders]
538
+ );
539
+ const totalUnread = Object.values(unreadCounts).reduce((sum, count) => sum + count, 0);
540
+ if (!socket) return null;
541
+ return /* @__PURE__ */ jsxRuntime.jsx(
542
+ CollabContext.Provider,
543
+ {
544
+ value: {
545
+ socket,
546
+ config,
547
+ apiBaseUrl,
548
+ totalUnread,
549
+ requestPreview,
550
+ invalidatePreview,
551
+ fetchMessages,
552
+ createConversationWithMessage,
553
+ addParticipant,
554
+ removeParticipant,
555
+ fetchConversationList,
556
+ pinMessage,
557
+ unpinMessage,
558
+ fetchPinnedMessages
559
+ },
560
+ children
561
+ }
562
+ );
563
+ }
564
+
565
+ // src/core/channelName.ts
566
+ function buildChannelName(patientData) {
567
+ const patientPart = patientData.patientName?.trim() || "Unknown";
568
+ const labPart = patientData.labName?.trim()?.slice(0, 24) || "";
569
+ const last4 = (patientData.displayOrderId ?? "").trim().slice(-4) || "0000";
570
+ return `${patientPart} - ${labPart} - ${last4}`;
571
+ }
572
+
573
+ // src/hooks/useConversation.ts
574
+ function useConversation({
575
+ orderId,
576
+ patientData,
577
+ participantIds = [],
578
+ loadHistory = true
579
+ }) {
580
+ const {
581
+ socket,
582
+ config,
583
+ requestPreview,
584
+ invalidatePreview,
585
+ fetchMessages,
586
+ createConversationWithMessage,
587
+ pinMessage: pinMessageApi,
588
+ unpinMessage: unpinMessageApi
589
+ } = useCollab();
590
+ const [conversation, setConversation] = react.useState(null);
591
+ const [messages, setMessages] = react.useState([]);
592
+ const [pinnedMessages, setPinnedMessages] = react.useState([]);
593
+ const [participants, setParticipants] = react.useState([]);
594
+ const [typingUsers, setTypingUsers] = react.useState([]);
595
+ const [isLoading, setIsLoading] = react.useState(true);
596
+ const [hasMore, setHasMore] = react.useState(true);
597
+ const [error, setError] = react.useState(null);
598
+ const [isConnected, setIsConnected] = react.useState(false);
599
+ const [replyTo, setReplyTo] = react.useState(null);
600
+ const joinedConversationId = react.useRef(null);
601
+ const typingTimers = react.useRef(/* @__PURE__ */ new Map());
602
+ const ensureConversationInFlight = react.useRef(null);
603
+ const markedReadIdsRef = react.useRef(/* @__PURE__ */ new Set());
604
+ const buildName = react.useCallback(
605
+ () => buildChannelName(patientData),
606
+ [patientData]
607
+ );
608
+ const joinChannel = react.useCallback(
609
+ (conv) => {
610
+ if (joinedConversationId.current === conv.id) return;
611
+ socket.joinConversation(conv.id, {
612
+ // Dedupe by id so an optimistic message (added client-side on send)
613
+ // doesn't double up when the server's broadcast arrives.
614
+ onMessage: (msg) => setMessages((prev) => {
615
+ if (prev.some((m) => m.id === msg.id)) return prev;
616
+ const optimisticIdx = prev.findIndex(
617
+ (m) => m.id.startsWith("pending-") && m.senderId === msg.senderId && m.body === msg.body
618
+ );
619
+ if (optimisticIdx >= 0) {
620
+ const next = prev.slice();
621
+ next[optimisticIdx] = msg;
622
+ return next;
623
+ }
624
+ return [...prev, msg];
625
+ }),
626
+ onTyping: (event) => {
627
+ if (event.userId === config.userId) return;
628
+ if (event.isTyping) {
629
+ setTypingUsers((prev) => {
630
+ const exists = prev.some((t) => t.userId === event.userId);
631
+ return exists ? prev : [...prev, event];
632
+ });
633
+ const existingTimer = typingTimers.current.get(event.userId);
634
+ if (existingTimer) clearTimeout(existingTimer);
635
+ const timer = setTimeout(() => {
636
+ setTypingUsers((prev) => prev.filter((t) => t.userId !== event.userId));
637
+ typingTimers.current.delete(event.userId);
638
+ }, 3e3);
639
+ typingTimers.current.set(event.userId, timer);
640
+ } else {
641
+ setTypingUsers((prev) => prev.filter((t) => t.userId !== event.userId));
642
+ const timer = typingTimers.current.get(event.userId);
643
+ if (timer) clearTimeout(timer);
644
+ typingTimers.current.delete(event.userId);
645
+ }
646
+ },
647
+ onUserJoined: (participant) => {
648
+ setParticipants(
649
+ (prev) => prev.some((p) => p.userId === participant.userId) ? prev : [...prev, participant]
650
+ );
651
+ },
652
+ onUserLeft: (participant) => {
653
+ setParticipants((prev) => prev.filter((p) => p.userId !== participant.userId));
654
+ },
655
+ onChannelUpdated: (updates) => {
656
+ setConversation((prev) => prev ? { ...prev, ...updates } : prev);
657
+ },
658
+ onChannelDeleted: () => {
659
+ setConversation(null);
660
+ setMessages([]);
661
+ joinedConversationId.current = null;
662
+ setIsConnected(false);
663
+ },
664
+ onMessageRead: ({ messageId, userId }) => {
665
+ setMessages(
666
+ (prev) => prev.map(
667
+ (msg) => msg.id === messageId && !(msg.readBy ?? []).includes(userId) ? { ...msg, readBy: [...msg.readBy ?? [], userId] } : msg
668
+ )
669
+ );
670
+ },
671
+ onMessagePinned: (message) => {
672
+ setPinnedMessages(
673
+ (prev) => prev.some((m) => m.id === message.id) ? prev : [...prev, message]
674
+ );
675
+ setMessages(
676
+ (prev) => prev.map((m) => m.id === message.id ? { ...m, isPinned: true } : m)
677
+ );
678
+ },
679
+ onMessageUnpinned: ({ messageId }) => {
680
+ setPinnedMessages((prev) => prev.filter((m) => m.id !== messageId));
681
+ setMessages(
682
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, isPinned: false } : m)
683
+ );
684
+ }
685
+ });
686
+ joinedConversationId.current = conv.id;
687
+ setIsConnected(true);
688
+ },
689
+ [socket, config.userId]
690
+ );
691
+ react.useEffect(() => {
692
+ let cancelled = false;
693
+ const init = async () => {
694
+ setIsLoading(true);
695
+ setError(null);
696
+ try {
697
+ const preview = await requestPreview(orderId);
698
+ if (cancelled) return;
699
+ if (!preview) {
700
+ setConversation(null);
701
+ setMessages([]);
702
+ setParticipants([]);
703
+ setHasMore(false);
704
+ setIsLoading(false);
705
+ return;
706
+ }
707
+ const existingConv = {
708
+ id: preview.conversationId,
709
+ orderId: preview.orderId,
710
+ name: preview.name,
711
+ participants: preview.participants,
712
+ unreadCount: preview.unreadCount,
713
+ createdAt: "",
714
+ updatedAt: ""
715
+ };
716
+ setConversation(existingConv);
717
+ setParticipants(preview.participants);
718
+ joinChannel(existingConv);
719
+ setMessages(preview.lastMessages);
720
+ setHasMore(preview.messageCount > preview.lastMessages.length);
721
+ setPinnedMessages(preview.lastMessages.filter((m) => m.isPinned));
722
+ setIsLoading(false);
723
+ if (loadHistory) {
724
+ fetchMessages(preview.conversationId).then((history) => {
725
+ if (cancelled) return;
726
+ setMessages((prev) => {
727
+ const seen = new Set(prev.map((m) => m.id));
728
+ const older = history.filter((m) => !seen.has(m.id));
729
+ return [...older, ...prev];
730
+ });
731
+ setHasMore(history.length > 0);
732
+ setPinnedMessages((prev) => {
733
+ const fromHistory = history.filter((m) => m.isPinned);
734
+ const seen = new Set(prev.map((m) => m.id));
735
+ return [...fromHistory.filter((m) => !seen.has(m.id)), ...prev];
736
+ });
737
+ }).catch(() => {
738
+ });
739
+ }
740
+ } catch (err) {
741
+ if (cancelled) return;
742
+ const msg = err instanceof Error ? err.message : "Failed to load conversation";
743
+ setError(msg);
744
+ setIsLoading(false);
745
+ }
746
+ };
747
+ init();
748
+ return () => {
749
+ cancelled = true;
750
+ if (joinedConversationId.current) {
751
+ socket.leaveConversation(joinedConversationId.current);
752
+ joinedConversationId.current = null;
753
+ }
754
+ typingTimers.current.forEach((timer) => clearTimeout(timer));
755
+ typingTimers.current.clear();
756
+ markedReadIdsRef.current.clear();
757
+ setIsConnected(false);
758
+ };
759
+ }, [orderId]);
760
+ const ensureConversation = react.useCallback(
761
+ async (payload) => {
762
+ if (conversation) return { conv: conversation, persistedByCreate: false };
763
+ if (ensureConversationInFlight.current) {
764
+ const conv = await ensureConversationInFlight.current;
765
+ return { conv, persistedByCreate: false };
766
+ }
767
+ const inflight = (async () => {
768
+ const result = await createConversationWithMessage(orderId, payload, {
769
+ name: buildName(),
770
+ participantIds
771
+ });
772
+ setConversation(result.conversation);
773
+ setParticipants(result.conversation.participants);
774
+ setMessages([result.message]);
775
+ joinChannel(result.conversation);
776
+ invalidatePreview(orderId);
777
+ if (loadHistory) {
778
+ fetchMessages(result.conversation.id).then((history) => {
779
+ setMessages((prev) => {
780
+ const seen = new Set(prev.map((m) => m.id));
781
+ const older = history.filter((m) => !seen.has(m.id));
782
+ return [...older, ...prev];
783
+ });
784
+ setPinnedMessages((prev) => {
785
+ const fromHistory = history.filter((m) => m.isPinned);
786
+ const seen = new Set(prev.map((m) => m.id));
787
+ return [...fromHistory.filter((m) => !seen.has(m.id)), ...prev];
788
+ });
789
+ }).catch(() => {
790
+ });
791
+ }
792
+ return result.conversation;
793
+ })();
794
+ ensureConversationInFlight.current = inflight;
795
+ try {
796
+ const conv = await inflight;
797
+ return { conv, persistedByCreate: true };
798
+ } finally {
799
+ ensureConversationInFlight.current = null;
800
+ }
801
+ },
802
+ [
803
+ conversation,
804
+ createConversationWithMessage,
805
+ orderId,
806
+ buildName,
807
+ participantIds,
808
+ joinChannel,
809
+ invalidatePreview,
810
+ loadHistory,
811
+ fetchMessages
812
+ ]
813
+ );
814
+ const sendMessage = react.useCallback(
815
+ async (body) => {
816
+ const payload = {
817
+ body,
818
+ type: "text",
819
+ ...replyTo ? { replyToId: replyTo.id } : {}
820
+ };
821
+ const { conv, persistedByCreate } = await ensureConversation(payload);
822
+ setReplyTo(null);
823
+ if (persistedByCreate) return;
824
+ const optimistic = {
825
+ id: `pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
826
+ conversationId: conv.id,
827
+ senderId: config.userId,
828
+ senderName: config.userName,
829
+ senderRole: config.userRole,
830
+ body,
831
+ type: "text",
832
+ insertedAt: (/* @__PURE__ */ new Date()).toISOString(),
833
+ ...replyTo ? {
834
+ replyToId: replyTo.id,
835
+ replyToSnapshot: {
836
+ messageId: replyTo.id,
837
+ senderId: replyTo.senderId,
838
+ senderName: replyTo.senderName,
839
+ body: replyTo.body,
840
+ type: replyTo.type
841
+ }
842
+ } : {}
843
+ };
844
+ setMessages((prev) => [...prev, optimistic]);
845
+ try {
846
+ await socket.sendMessage(conv.id, payload);
847
+ } catch (err) {
848
+ setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
849
+ config.onError?.({
850
+ code: "SEND_FAILED",
851
+ message: "Failed to send message",
852
+ details: err
853
+ });
854
+ }
855
+ },
856
+ [ensureConversation, socket, replyTo, config]
857
+ );
858
+ const sendAudioMessage = react.useCallback(
859
+ async (audioBlob, duration) => {
860
+ if (!config.onUploadFile) return;
861
+ const url = await config.onUploadFile(audioBlob, "voice-message.webm");
862
+ const payload = {
863
+ body: "Voice message",
864
+ type: "audio",
865
+ mediaUrl: url,
866
+ mediaDuration: duration
867
+ };
868
+ const { conv, persistedByCreate } = await ensureConversation(payload);
869
+ if (persistedByCreate) return;
870
+ await socket.sendMessage(conv.id, payload);
871
+ },
872
+ [config, ensureConversation, socket]
873
+ );
874
+ const sendFileMessage = react.useCallback(
875
+ async (file) => {
876
+ if (!config.onUploadFile) return;
877
+ const url = await config.onUploadFile(file, file.name);
878
+ const isImage = file.type.startsWith("image/");
879
+ const payload = {
880
+ body: file.name,
881
+ type: isImage ? "image" : "file",
882
+ mediaUrl: url,
883
+ fileName: file.name
884
+ };
885
+ const { conv, persistedByCreate } = await ensureConversation(payload);
886
+ if (persistedByCreate) return;
887
+ await socket.sendMessage(conv.id, payload);
888
+ },
889
+ [config, ensureConversation, socket]
890
+ );
891
+ const sendDeepLink = react.useCallback(
892
+ async (path, label) => {
893
+ const payload = {
894
+ body: label,
895
+ type: "deep_link",
896
+ metadata: { path }
897
+ };
898
+ const { conv, persistedByCreate } = await ensureConversation(payload);
899
+ if (persistedByCreate) return;
900
+ await socket.sendMessage(conv.id, payload);
901
+ },
902
+ [ensureConversation, socket]
903
+ );
904
+ const sendTypingIndicator = react.useCallback(
905
+ (isTyping) => {
906
+ if (!conversation || !isConnected) return;
907
+ socket.sendTyping(conversation.id, isTyping);
908
+ },
909
+ [conversation, isConnected, socket]
910
+ );
911
+ const markAsRead = react.useCallback(
912
+ (messageId) => {
913
+ if (!conversation || !isConnected) return;
914
+ if (markedReadIdsRef.current.has(messageId)) return;
915
+ markedReadIdsRef.current.add(messageId);
916
+ socket.markAsRead(conversation.id, messageId);
917
+ },
918
+ [conversation, isConnected, socket]
919
+ );
920
+ const loadMoreMessages = react.useCallback(async () => {
921
+ if (!conversation || !hasMore || isLoading) return;
922
+ const oldestMessage = messages[0];
923
+ const before = oldestMessage?.id;
924
+ setIsLoading(true);
925
+ try {
926
+ const older = await fetchMessages(conversation.id, { before });
927
+ setMessages((prev) => [...older, ...prev]);
928
+ setHasMore(older.length > 0);
929
+ } catch (err) {
930
+ config.onError?.({
931
+ code: "LOAD_ERROR",
932
+ message: "Failed to load older messages",
933
+ details: err
934
+ });
935
+ } finally {
936
+ setIsLoading(false);
937
+ }
938
+ }, [conversation, hasMore, isLoading, messages, fetchMessages, config]);
939
+ const pinMessage = react.useCallback(
940
+ async (messageId) => {
941
+ if (!conversation) return;
942
+ try {
943
+ await pinMessageApi(conversation.id, messageId);
944
+ } catch (err) {
945
+ config.onError?.({
946
+ code: "PIN_ERROR",
947
+ message: "Failed to pin message",
948
+ details: err
949
+ });
950
+ }
951
+ },
952
+ [conversation, pinMessageApi, config]
953
+ );
954
+ const unpinMessage = react.useCallback(
955
+ async (messageId) => {
956
+ if (!conversation) return;
957
+ try {
958
+ await unpinMessageApi(conversation.id, messageId);
959
+ } catch (err) {
960
+ config.onError?.({
961
+ code: "UNPIN_ERROR",
962
+ message: "Failed to unpin message",
963
+ details: err
964
+ });
965
+ }
966
+ },
967
+ [conversation, unpinMessageApi, config]
968
+ );
969
+ return {
970
+ conversation,
971
+ messages,
972
+ pinnedMessages,
973
+ participants,
974
+ typingUsers,
975
+ isLoading,
976
+ hasMore,
977
+ error,
978
+ isConnected,
979
+ replyTo,
980
+ setReplyTo,
981
+ sendMessage,
982
+ sendAudioMessage,
983
+ sendFileMessage,
984
+ sendDeepLink,
985
+ sendTyping: sendTypingIndicator,
986
+ markAsRead,
987
+ loadMoreMessages,
988
+ pinMessage,
989
+ unpinMessage
990
+ };
991
+ }
992
+ function useChannelSettings({ conversationId }) {
993
+ const { socket, config } = useCollab();
994
+ const [isUpdating, setIsUpdating] = react.useState(false);
995
+ const [error, setError] = react.useState(null);
996
+ const isAdmin = config.userRole === "admin";
997
+ const withLoading = react.useCallback(
998
+ async (operation) => {
999
+ if (!conversationId) return;
1000
+ setIsUpdating(true);
1001
+ setError(null);
1002
+ try {
1003
+ const result = await operation();
1004
+ setIsUpdating(false);
1005
+ return result;
1006
+ } catch (err) {
1007
+ const collabError = typeof err === "object" && err !== null && "code" in err ? err : { code: "UNKNOWN", message: "Operation failed" };
1008
+ setError(collabError);
1009
+ setIsUpdating(false);
1010
+ config.onError?.(collabError);
1011
+ return void 0;
1012
+ }
1013
+ },
1014
+ [conversationId, config]
1015
+ );
1016
+ const inviteUser = react.useCallback(
1017
+ async (payload) => {
1018
+ await withLoading(() => socket.inviteUser(conversationId, payload));
1019
+ },
1020
+ [conversationId, socket, withLoading]
1021
+ );
1022
+ const removeUser = react.useCallback(
1023
+ async (userId) => {
1024
+ await withLoading(() => socket.removeUser(conversationId, userId));
1025
+ },
1026
+ [conversationId, socket, withLoading]
1027
+ );
1028
+ const leaveChannel = react.useCallback(async () => {
1029
+ await withLoading(async () => {
1030
+ await socket.removeUser(conversationId, config.userId);
1031
+ socket.leaveConversation(conversationId);
1032
+ });
1033
+ }, [conversationId, socket, config.userId, withLoading]);
1034
+ const deleteChannel = react.useCallback(async () => {
1035
+ if (!isAdmin) {
1036
+ const err = {
1037
+ code: "FORBIDDEN",
1038
+ message: "Only admins can delete channels"
1039
+ };
1040
+ setError(err);
1041
+ config.onError?.(err);
1042
+ return;
1043
+ }
1044
+ await withLoading(() => socket.deleteChannel(conversationId));
1045
+ }, [conversationId, socket, isAdmin, config, withLoading]);
1046
+ const updateChannel = react.useCallback(
1047
+ async (payload) => {
1048
+ await withLoading(() => socket.updateChannel(conversationId, payload));
1049
+ },
1050
+ [conversationId, socket, withLoading]
1051
+ );
1052
+ return {
1053
+ inviteUser,
1054
+ removeUser,
1055
+ leaveChannel,
1056
+ deleteChannel,
1057
+ updateChannel,
1058
+ isUpdating,
1059
+ error,
1060
+ isAdmin
1061
+ };
1062
+ }
1063
+ function useDeepLinks() {
1064
+ const { config } = useCollab();
1065
+ const parseDeepLink = react.useCallback((link) => {
1066
+ if (!link.startsWith(DEEP_LINK_PREFIX)) return null;
1067
+ const path = link.slice(DEEP_LINK_PREFIX.length);
1068
+ const parts = path.split("/").filter(Boolean);
1069
+ if (parts.length < 2) return null;
1070
+ return {
1071
+ raw: link,
1072
+ path,
1073
+ resource: parts[0],
1074
+ id: parts[1]
1075
+ };
1076
+ }, []);
1077
+ const handleDeepLink = react.useCallback(
1078
+ (link) => {
1079
+ const parsed = parseDeepLink(link);
1080
+ if (!parsed) return;
1081
+ if (parsed.resource === "dicom" && config.onOpenDicom) {
1082
+ config.onOpenDicom(parsed.id, "");
1083
+ return;
1084
+ }
1085
+ if (config.onDeepLink) {
1086
+ config.onDeepLink(parsed.path);
1087
+ }
1088
+ },
1089
+ [parseDeepLink, config]
1090
+ );
1091
+ const containsDeepLink = react.useCallback((text) => {
1092
+ return text.includes(DEEP_LINK_PREFIX);
1093
+ }, []);
1094
+ const extractDeepLinks = react.useCallback(
1095
+ (text) => {
1096
+ const regex = new RegExp(`${escapeRegex(DEEP_LINK_PREFIX)}[\\w/.-]+`, "g");
1097
+ const matches = text.match(regex) || [];
1098
+ return matches.map(parseDeepLink).filter((link) => link !== null);
1099
+ },
1100
+ [parseDeepLink]
1101
+ );
1102
+ return {
1103
+ parseDeepLink,
1104
+ handleDeepLink,
1105
+ containsDeepLink,
1106
+ extractDeepLinks
1107
+ };
1108
+ }
1109
+ function escapeRegex(string) {
1110
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1111
+ }
1112
+ function PatientHeader({
1113
+ patientData,
1114
+ onOpenDicom,
1115
+ onOpenSettings,
1116
+ className
1117
+ }) {
1118
+ const hasDicom = !!(patientData.studyId && patientData.storageId);
1119
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.container, children: [
1120
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.infoRow, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.patientInfo, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.patientName, children: buildChannelName(patientData) }) }) }),
1121
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.secondaryRow, children: [
1122
+ patientData.patientAge && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.secondary, children: [
1123
+ patientData.patientAge,
1124
+ patientData.patientSex ? `/${patientData.patientSex}` : ""
1125
+ ] }),
1126
+ patientData.studyType && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.secondary, children: patientData.studyType }),
1127
+ patientData.bodyParts && patientData.bodyParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.secondary, children: patientData.bodyParts.join(", ") }),
1128
+ patientData.referringPhysician && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.secondary, children: [
1129
+ "Ref: ",
1130
+ patientData.referringPhysician
1131
+ ] })
1132
+ ] }),
1133
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
1134
+ hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpenDicom, style: styles.dicomButton, type: "button", children: "View DICOM" }),
1135
+ onOpenSettings && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpenSettings, style: styles.settingsButton, type: "button", children: "Settings" })
1136
+ ] })
1137
+ ] });
1138
+ }
1139
+ var styles = {
1140
+ container: {
1141
+ padding: "12px 16px",
1142
+ borderBottom: "1px solid #e5e7eb",
1143
+ backgroundColor: "#f9fafb"
1144
+ },
1145
+ infoRow: {
1146
+ display: "flex",
1147
+ justifyContent: "space-between",
1148
+ alignItems: "center"
1149
+ },
1150
+ patientInfo: {
1151
+ display: "flex",
1152
+ alignItems: "center",
1153
+ gap: "8px",
1154
+ flexWrap: "wrap"
1155
+ },
1156
+ patientName: {
1157
+ fontWeight: 600,
1158
+ fontSize: "15px",
1159
+ color: "#111827"
1160
+ },
1161
+ secondaryRow: {
1162
+ display: "flex",
1163
+ gap: "12px",
1164
+ marginTop: "4px",
1165
+ alignItems: "center"
1166
+ },
1167
+ secondary: {
1168
+ fontSize: "12px",
1169
+ color: "#6b7280"
1170
+ },
1171
+ actions: {
1172
+ display: "flex",
1173
+ gap: "8px",
1174
+ marginTop: "8px"
1175
+ },
1176
+ dicomButton: {
1177
+ padding: "6px 12px",
1178
+ fontSize: "13px",
1179
+ fontWeight: 500,
1180
+ color: "#ffffff",
1181
+ backgroundColor: "#2563eb",
1182
+ border: "none",
1183
+ borderRadius: "6px",
1184
+ cursor: "pointer"
1185
+ },
1186
+ settingsButton: {
1187
+ padding: "6px 12px",
1188
+ fontSize: "13px",
1189
+ fontWeight: 500,
1190
+ color: "#374151",
1191
+ backgroundColor: "#ffffff",
1192
+ border: "1px solid #d1d5db",
1193
+ borderRadius: "6px",
1194
+ cursor: "pointer"
1195
+ }
1196
+ };
1197
+ function ReplyQuoteBlock({
1198
+ snapshot,
1199
+ inOwnBubble = false,
1200
+ onClick,
1201
+ className
1202
+ }) {
1203
+ const baseBg = inOwnBubble ? "rgba(255,255,255,0.18)" : "#f3f4f6";
1204
+ const barColor = inOwnBubble ? "#ffffff" : "#2563eb";
1205
+ const nameColor = inOwnBubble ? "#ffffff" : "#2563eb";
1206
+ const bodyColor = inOwnBubble ? "rgba(255,255,255,0.85)" : "#4b5563";
1207
+ const preview = renderSnapshotPreview(snapshot);
1208
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1209
+ "div",
1210
+ {
1211
+ className,
1212
+ style: { ...styles2.container, backgroundColor: baseBg },
1213
+ onClick: () => onClick?.(snapshot.messageId),
1214
+ role: onClick ? "button" : void 0,
1215
+ tabIndex: onClick ? 0 : void 0,
1216
+ children: [
1217
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...styles2.bar, backgroundColor: barColor } }),
1218
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles2.content, children: [
1219
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles2.senderName, color: nameColor }, children: snapshot.senderName }),
1220
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles2.body, color: bodyColor }, children: preview })
1221
+ ] })
1222
+ ]
1223
+ }
1224
+ );
1225
+ }
1226
+ function renderSnapshotPreview(snapshot) {
1227
+ switch (snapshot.type) {
1228
+ case "audio":
1229
+ return "\u{1F3A4} Voice message";
1230
+ case "image":
1231
+ return "\u{1F4F7} Image";
1232
+ case "file":
1233
+ return "\u{1F4CE} File";
1234
+ case "deep_link":
1235
+ return `\u{1F517} ${snapshot.body}`;
1236
+ default: {
1237
+ const body = snapshot.body || "";
1238
+ return body.length > 80 ? `${body.slice(0, 80)}\u2026` : body;
1239
+ }
1240
+ }
1241
+ }
1242
+ var styles2 = {
1243
+ container: {
1244
+ display: "flex",
1245
+ gap: "6px",
1246
+ padding: "4px 8px",
1247
+ marginBottom: "4px",
1248
+ borderRadius: "6px",
1249
+ cursor: "pointer",
1250
+ maxWidth: "100%"
1251
+ },
1252
+ bar: {
1253
+ width: "3px",
1254
+ borderRadius: "2px",
1255
+ flexShrink: 0
1256
+ },
1257
+ content: {
1258
+ display: "flex",
1259
+ flexDirection: "column",
1260
+ gap: "1px",
1261
+ minWidth: 0,
1262
+ overflow: "hidden"
1263
+ },
1264
+ senderName: {
1265
+ fontSize: "11px",
1266
+ fontWeight: 600,
1267
+ lineHeight: "1.2"
1268
+ },
1269
+ body: {
1270
+ fontSize: "11px",
1271
+ lineHeight: "1.3",
1272
+ overflow: "hidden",
1273
+ textOverflow: "ellipsis",
1274
+ whiteSpace: "nowrap"
1275
+ }
1276
+ };
1277
+ function SeenByIndicator({
1278
+ readBy,
1279
+ participants,
1280
+ currentUserId,
1281
+ senderId,
1282
+ className
1283
+ }) {
1284
+ const [expanded, setExpanded] = react.useState(false);
1285
+ if (senderId !== currentUserId) return null;
1286
+ const seenByOthers = readBy.filter((id) => id !== currentUserId && id !== senderId).map((id) => participants.find((p) => p.userId === id)).filter((p) => !!p);
1287
+ if (seenByOthers.length === 0) return null;
1288
+ const displayCount = expanded ? seenByOthers.length : Math.min(3, seenByOthers.length);
1289
+ const shown = seenByOthers.slice(0, displayCount);
1290
+ const remainder = seenByOthers.length - displayCount;
1291
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles3.container, children: [
1292
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles3.checkmark, children: "\u2713\u2713" }),
1293
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles3.label, children: [
1294
+ "Seen by ",
1295
+ shown.map((p) => p.userName ?? "User").join(", ")
1296
+ ] }),
1297
+ remainder > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1298
+ "button",
1299
+ {
1300
+ onClick: (e) => {
1301
+ e.stopPropagation();
1302
+ setExpanded(true);
1303
+ },
1304
+ style: styles3.moreButton,
1305
+ type: "button",
1306
+ children: [
1307
+ "+",
1308
+ remainder
1309
+ ]
1310
+ }
1311
+ )
1312
+ ] });
1313
+ }
1314
+ var styles3 = {
1315
+ container: {
1316
+ display: "flex",
1317
+ alignItems: "center",
1318
+ gap: "4px",
1319
+ marginTop: "2px",
1320
+ fontSize: "10px",
1321
+ opacity: 0.75
1322
+ },
1323
+ checkmark: {
1324
+ fontSize: "10px",
1325
+ color: "#60a5fa"
1326
+ },
1327
+ label: {
1328
+ fontSize: "10px",
1329
+ color: "inherit"
1330
+ },
1331
+ moreButton: {
1332
+ fontSize: "10px",
1333
+ fontWeight: 600,
1334
+ color: "inherit",
1335
+ backgroundColor: "transparent",
1336
+ border: "none",
1337
+ padding: 0,
1338
+ cursor: "pointer",
1339
+ textDecoration: "underline"
1340
+ }
1341
+ };
1342
+ function MessageActionsMenu({
1343
+ message,
1344
+ onReply,
1345
+ onPin,
1346
+ onUnpin,
1347
+ onCopy,
1348
+ visible,
1349
+ pinDisabled = false,
1350
+ className
1351
+ }) {
1352
+ if (!visible) return null;
1353
+ const isPinned = !!message.isPinned;
1354
+ const canCopy = message.type === "text" && !!message.body;
1355
+ const handleCopy = () => {
1356
+ if (!onCopy) return;
1357
+ onCopy(message.body);
1358
+ };
1359
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles4.container, children: [
1360
+ /* @__PURE__ */ jsxRuntime.jsx(
1361
+ "button",
1362
+ {
1363
+ onClick: () => onReply(message),
1364
+ style: styles4.button,
1365
+ title: "Reply",
1366
+ type: "button",
1367
+ children: "\u21A9"
1368
+ }
1369
+ ),
1370
+ isPinned ? /* @__PURE__ */ jsxRuntime.jsx(
1371
+ "button",
1372
+ {
1373
+ onClick: () => onUnpin(message.id),
1374
+ style: styles4.button,
1375
+ title: "Unpin",
1376
+ type: "button",
1377
+ children: "\u{1F4CC}"
1378
+ }
1379
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1380
+ "button",
1381
+ {
1382
+ onClick: () => onPin(message.id),
1383
+ style: {
1384
+ ...styles4.button,
1385
+ opacity: pinDisabled ? 0.4 : 1,
1386
+ cursor: pinDisabled ? "not-allowed" : "pointer"
1387
+ },
1388
+ disabled: pinDisabled,
1389
+ title: pinDisabled ? "Pin limit reached (max 3)" : "Pin",
1390
+ type: "button",
1391
+ children: "\u{1F4CC}"
1392
+ }
1393
+ ),
1394
+ canCopy && onCopy && /* @__PURE__ */ jsxRuntime.jsx(
1395
+ "button",
1396
+ {
1397
+ onClick: handleCopy,
1398
+ style: styles4.button,
1399
+ title: "Copy text",
1400
+ type: "button",
1401
+ children: "\u2398"
1402
+ }
1403
+ )
1404
+ ] });
1405
+ }
1406
+ var styles4 = {
1407
+ container: {
1408
+ display: "flex",
1409
+ gap: "2px",
1410
+ padding: "2px",
1411
+ backgroundColor: "#ffffff",
1412
+ border: "1px solid #e5e7eb",
1413
+ borderRadius: "20px",
1414
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.08)"
1415
+ },
1416
+ button: {
1417
+ width: "24px",
1418
+ height: "24px",
1419
+ display: "flex",
1420
+ alignItems: "center",
1421
+ justifyContent: "center",
1422
+ fontSize: "12px",
1423
+ backgroundColor: "transparent",
1424
+ border: "none",
1425
+ borderRadius: "50%",
1426
+ cursor: "pointer"
1427
+ }
1428
+ };
1429
+ function MessageBubble({
1430
+ message,
1431
+ isOwn,
1432
+ participants = [],
1433
+ currentUserId = "",
1434
+ showSeenBy = true,
1435
+ onDeepLinkClick,
1436
+ onReply,
1437
+ onPin,
1438
+ onUnpin,
1439
+ onCopy,
1440
+ onReplyJumpTo,
1441
+ pinDisabled = false,
1442
+ className
1443
+ }) {
1444
+ const [isHovered, setIsHovered] = react.useState(false);
1445
+ if (message.type === "system") {
1446
+ return /* @__PURE__ */ jsxRuntime.jsx(SystemBubble, { message });
1447
+ }
1448
+ const hasActions = !!(onReply || onPin || onUnpin || onCopy);
1449
+ return /* @__PURE__ */ jsxRuntime.jsx(
1450
+ "div",
1451
+ {
1452
+ className,
1453
+ style: {
1454
+ ...styles5.wrapper,
1455
+ justifyContent: isOwn ? "flex-end" : "flex-start"
1456
+ },
1457
+ onMouseEnter: () => setIsHovered(true),
1458
+ onMouseLeave: () => setIsHovered(false),
1459
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles5.bubbleWrapper, children: [
1460
+ /* @__PURE__ */ jsxRuntime.jsxs(
1461
+ "div",
1462
+ {
1463
+ style: {
1464
+ ...styles5.bubble,
1465
+ ...isOwn ? styles5.ownBubble : styles5.otherBubble
1466
+ },
1467
+ children: [
1468
+ message.isPinned && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles5.pinnedBadge, children: "\u{1F4CC} Pinned" }),
1469
+ message.replyToSnapshot && /* @__PURE__ */ jsxRuntime.jsx(
1470
+ ReplyQuoteBlock,
1471
+ {
1472
+ snapshot: message.replyToSnapshot,
1473
+ inOwnBubble: isOwn,
1474
+ onClick: onReplyJumpTo
1475
+ }
1476
+ ),
1477
+ !isOwn && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles5.senderRow, children: [
1478
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.senderName, children: message.senderName }),
1479
+ /* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: message.senderRole })
1480
+ ] }),
1481
+ message.type === "text" && /* @__PURE__ */ jsxRuntime.jsx(TextContent, { body: message.body, onDeepLinkClick }),
1482
+ message.type === "audio" && /* @__PURE__ */ jsxRuntime.jsx(AudioContent, { mediaUrl: message.mediaUrl, duration: message.mediaDuration }),
1483
+ message.type === "image" && /* @__PURE__ */ jsxRuntime.jsx(ImageContent, { mediaUrl: message.mediaUrl, fileName: message.fileName }),
1484
+ message.type === "file" && /* @__PURE__ */ jsxRuntime.jsx(FileContent, { mediaUrl: message.mediaUrl, fileName: message.fileName }),
1485
+ message.type === "deep_link" && /* @__PURE__ */ jsxRuntime.jsx(DeepLinkContent, { body: message.body, metadata: message.metadata, onDeepLinkClick }),
1486
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles5.timestamp, children: formatTime(message.insertedAt) }),
1487
+ showSeenBy && isOwn && /* @__PURE__ */ jsxRuntime.jsx(
1488
+ SeenByIndicator,
1489
+ {
1490
+ readBy: message.readBy ?? [],
1491
+ participants,
1492
+ currentUserId,
1493
+ senderId: message.senderId
1494
+ }
1495
+ )
1496
+ ]
1497
+ }
1498
+ ),
1499
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
1500
+ "div",
1501
+ {
1502
+ style: {
1503
+ ...styles5.actionsWrapper,
1504
+ [isOwn ? "right" : "left"]: "100%",
1505
+ [isOwn ? "marginRight" : "marginLeft"]: "6px"
1506
+ },
1507
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1508
+ MessageActionsMenu,
1509
+ {
1510
+ message,
1511
+ onReply: onReply ?? (() => {
1512
+ }),
1513
+ onPin: onPin ?? (() => {
1514
+ }),
1515
+ onUnpin: onUnpin ?? (() => {
1516
+ }),
1517
+ onCopy,
1518
+ visible: isHovered,
1519
+ pinDisabled
1520
+ }
1521
+ )
1522
+ }
1523
+ )
1524
+ ] })
1525
+ }
1526
+ );
1527
+ }
1528
+ function TextContent({ body, onDeepLinkClick }) {
1529
+ if (body.includes(DEEP_LINK_PREFIX)) {
1530
+ const parts = body.split(new RegExp(`(${escapeRegex2(DEEP_LINK_PREFIX)}[\\w/.-]+)`, "g"));
1531
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles5.textBody, children: parts.map(
1532
+ (part, i) => part.startsWith(DEEP_LINK_PREFIX) ? /* @__PURE__ */ jsxRuntime.jsx(
1533
+ "a",
1534
+ {
1535
+ onClick: (e) => {
1536
+ e.preventDefault();
1537
+ onDeepLinkClick?.(part);
1538
+ },
1539
+ href: "#",
1540
+ style: styles5.deepLink,
1541
+ children: part
1542
+ },
1543
+ i
1544
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: part }, i)
1545
+ ) });
1546
+ }
1547
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles5.textBody, children: body });
1548
+ }
1549
+ function AudioContent({ mediaUrl, duration }) {
1550
+ if (!mediaUrl) return null;
1551
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles5.audioContainer, children: [
1552
+ /* @__PURE__ */ jsxRuntime.jsx("audio", { controls: true, preload: "metadata", style: styles5.audioPlayer, children: /* @__PURE__ */ jsxRuntime.jsx("source", { src: mediaUrl }) }),
1553
+ duration != null && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.audioDuration, children: formatDuration(duration) })
1554
+ ] });
1555
+ }
1556
+ function ImageContent({ mediaUrl, fileName }) {
1557
+ if (!mediaUrl) return null;
1558
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { href: mediaUrl, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: mediaUrl, alt: fileName || "Image", style: styles5.imagePreview }) });
1559
+ }
1560
+ function FileContent({ mediaUrl, fileName }) {
1561
+ if (!mediaUrl) return null;
1562
+ return /* @__PURE__ */ jsxRuntime.jsxs("a", { href: mediaUrl, target: "_blank", rel: "noopener noreferrer", style: styles5.fileLink, children: [
1563
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.fileIcon, children: "\u{1F4C4}" }),
1564
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.fileName, children: fileName || "Download file" })
1565
+ ] });
1566
+ }
1567
+ function DeepLinkContent({
1568
+ body,
1569
+ metadata,
1570
+ onDeepLinkClick
1571
+ }) {
1572
+ const path = metadata?.path;
1573
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1574
+ "div",
1575
+ {
1576
+ style: styles5.deepLinkCard,
1577
+ onClick: () => path && onDeepLinkClick?.(`${DEEP_LINK_PREFIX}${path}`),
1578
+ children: [
1579
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.deepLinkLabel, children: body }),
1580
+ path && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles5.deepLinkPath, children: [
1581
+ DEEP_LINK_PREFIX,
1582
+ path
1583
+ ] })
1584
+ ]
1585
+ }
1586
+ );
1587
+ }
1588
+ function SystemBubble({ message }) {
1589
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles5.systemMessage, children: [
1590
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: message.body }),
1591
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
1592
+ ] });
1593
+ }
1594
+ function RoleBadge({ role }) {
1595
+ const colors = {
1596
+ radiologist: "#7c3aed",
1597
+ lab: "#2563eb",
1598
+ physician: "#059669",
1599
+ admin: "#dc2626"
1600
+ };
1601
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles5.roleBadge, backgroundColor: `${colors[role]}15`, color: colors[role] }, children: role });
1602
+ }
1603
+ function formatTime(iso) {
1604
+ try {
1605
+ return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1606
+ } catch {
1607
+ return "";
1608
+ }
1609
+ }
1610
+ function formatDuration(seconds) {
1611
+ const m = Math.floor(seconds / 60);
1612
+ const s = seconds % 60;
1613
+ return `${m}:${s.toString().padStart(2, "0")}`;
1614
+ }
1615
+ function escapeRegex2(string) {
1616
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1617
+ }
1618
+ var styles5 = {
1619
+ wrapper: {
1620
+ display: "flex",
1621
+ marginBottom: "8px",
1622
+ paddingLeft: "16px",
1623
+ paddingRight: "16px"
1624
+ },
1625
+ bubbleWrapper: {
1626
+ position: "relative",
1627
+ maxWidth: "75%"
1628
+ },
1629
+ bubble: {
1630
+ padding: "8px 12px",
1631
+ borderRadius: "12px",
1632
+ wordBreak: "break-word"
1633
+ },
1634
+ ownBubble: {
1635
+ backgroundColor: "#2563eb",
1636
+ color: "#ffffff",
1637
+ borderBottomRightRadius: "4px"
1638
+ },
1639
+ otherBubble: {
1640
+ backgroundColor: "#f3f4f6",
1641
+ color: "#111827",
1642
+ borderBottomLeftRadius: "4px"
1643
+ },
1644
+ pinnedBadge: {
1645
+ display: "inline-block",
1646
+ fontSize: "10px",
1647
+ fontWeight: 600,
1648
+ marginBottom: "4px",
1649
+ opacity: 0.7
1650
+ },
1651
+ senderRow: {
1652
+ display: "flex",
1653
+ alignItems: "center",
1654
+ gap: "6px",
1655
+ marginBottom: "4px"
1656
+ },
1657
+ senderName: {
1658
+ fontSize: "12px",
1659
+ fontWeight: 600,
1660
+ color: "#374151"
1661
+ },
1662
+ roleBadge: {
1663
+ fontSize: "10px",
1664
+ fontWeight: 500,
1665
+ padding: "1px 6px",
1666
+ borderRadius: "4px"
1667
+ },
1668
+ textBody: {
1669
+ margin: 0,
1670
+ fontSize: "14px",
1671
+ lineHeight: "1.4"
1672
+ },
1673
+ timestamp: {
1674
+ fontSize: "10px",
1675
+ opacity: 0.7,
1676
+ marginTop: "4px",
1677
+ textAlign: "right"
1678
+ },
1679
+ actionsWrapper: {
1680
+ position: "absolute",
1681
+ top: "50%",
1682
+ transform: "translateY(-50%)",
1683
+ zIndex: 10
1684
+ },
1685
+ audioContainer: {
1686
+ display: "flex",
1687
+ alignItems: "center",
1688
+ gap: "8px"
1689
+ },
1690
+ audioPlayer: {
1691
+ height: "32px",
1692
+ maxWidth: "220px"
1693
+ },
1694
+ audioDuration: {
1695
+ fontSize: "12px",
1696
+ opacity: 0.7,
1697
+ whiteSpace: "nowrap"
1698
+ },
1699
+ imagePreview: {
1700
+ maxWidth: "240px",
1701
+ maxHeight: "180px",
1702
+ borderRadius: "8px",
1703
+ objectFit: "cover",
1704
+ cursor: "pointer"
1705
+ },
1706
+ fileLink: {
1707
+ display: "flex",
1708
+ alignItems: "center",
1709
+ gap: "6px",
1710
+ textDecoration: "none",
1711
+ color: "inherit",
1712
+ padding: "6px 0"
1713
+ },
1714
+ fileIcon: {
1715
+ fontSize: "18px"
1716
+ },
1717
+ fileName: {
1718
+ fontSize: "13px",
1719
+ textDecoration: "underline"
1720
+ },
1721
+ deepLink: {
1722
+ color: "#3b82f6",
1723
+ textDecoration: "underline",
1724
+ cursor: "pointer"
1725
+ },
1726
+ deepLinkCard: {
1727
+ display: "flex",
1728
+ flexDirection: "column",
1729
+ gap: "2px",
1730
+ padding: "8px",
1731
+ backgroundColor: "#eff6ff",
1732
+ borderRadius: "8px",
1733
+ border: "1px solid #bfdbfe",
1734
+ cursor: "pointer"
1735
+ },
1736
+ deepLinkLabel: {
1737
+ fontSize: "13px",
1738
+ fontWeight: 500,
1739
+ color: "#1d4ed8"
1740
+ },
1741
+ deepLinkPath: {
1742
+ fontSize: "11px",
1743
+ color: "#6b7280",
1744
+ fontFamily: "monospace"
1745
+ },
1746
+ systemMessage: {
1747
+ display: "flex",
1748
+ justifyContent: "center",
1749
+ alignItems: "center",
1750
+ gap: "8px",
1751
+ padding: "4px 16px",
1752
+ fontSize: "12px",
1753
+ color: "#9ca3af"
1754
+ },
1755
+ systemTime: {
1756
+ fontSize: "10px",
1757
+ color: "#d1d5db"
1758
+ }
1759
+ };
1760
+ var MessageList = react.forwardRef(function MessageList2({
1761
+ messages,
1762
+ currentUserId,
1763
+ participants = [],
1764
+ showSeenBy = true,
1765
+ typingUsers,
1766
+ hasMore,
1767
+ isLoading,
1768
+ onLoadMore,
1769
+ onDeepLinkClick,
1770
+ onMessageVisible,
1771
+ onReply,
1772
+ onPin,
1773
+ onUnpin,
1774
+ onCopy,
1775
+ pinDisabled,
1776
+ className
1777
+ }, ref) {
1778
+ const containerRef = react.useRef(null);
1779
+ const bottomRef = react.useRef(null);
1780
+ const prevMessageCount = react.useRef(messages.length);
1781
+ react.useImperativeHandle(
1782
+ ref,
1783
+ () => ({
1784
+ scrollToMessage: (messageId) => {
1785
+ const element = containerRef.current?.querySelector(`[data-message-id="${messageId}"]`);
1786
+ if (element) {
1787
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
1788
+ element.style.transition = "background-color 200ms";
1789
+ element.style.backgroundColor = "#fef3c7";
1790
+ setTimeout(() => {
1791
+ element.style.backgroundColor = "";
1792
+ }, 800);
1793
+ }
1794
+ }
1795
+ }),
1796
+ []
1797
+ );
1798
+ react.useEffect(() => {
1799
+ if (messages.length > prevMessageCount.current) {
1800
+ const lastMessage = messages[messages.length - 1];
1801
+ const isOwnMessage = lastMessage?.senderId === currentUserId;
1802
+ if (isOwnMessage || isNearBottom(containerRef.current)) {
1803
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1804
+ }
1805
+ }
1806
+ prevMessageCount.current = messages.length;
1807
+ }, [messages, currentUserId]);
1808
+ react.useEffect(() => {
1809
+ if (!onMessageVisible || !containerRef.current) return;
1810
+ const observer = new IntersectionObserver(
1811
+ (entries) => {
1812
+ entries.forEach((entry) => {
1813
+ if (entry.isIntersecting) {
1814
+ const messageId = entry.target.dataset.messageId;
1815
+ if (messageId) onMessageVisible(messageId);
1816
+ }
1817
+ });
1818
+ },
1819
+ { root: containerRef.current, threshold: 0.5 }
1820
+ );
1821
+ const messageElements = containerRef.current.querySelectorAll("[data-message-id]");
1822
+ messageElements.forEach((el) => observer.observe(el));
1823
+ return () => observer.disconnect();
1824
+ }, [messages, onMessageVisible]);
1825
+ const handleScroll = react.useCallback(() => {
1826
+ if (!containerRef.current || !hasMore || isLoading) return;
1827
+ if (containerRef.current.scrollTop < 50) {
1828
+ onLoadMore();
1829
+ }
1830
+ }, [hasMore, isLoading, onLoadMore]);
1831
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1832
+ "div",
1833
+ {
1834
+ ref: containerRef,
1835
+ className,
1836
+ style: styles6.container,
1837
+ onScroll: handleScroll,
1838
+ children: [
1839
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles6.loadingMore, children: "Loading earlier messages..." }),
1840
+ hasMore && !isLoading && messages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onLoadMore, style: styles6.loadMoreButton, type: "button", children: "Load earlier messages" }),
1841
+ messages.length === 0 && !isLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles6.empty, children: [
1842
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles6.emptyTitle, children: "No messages yet" }),
1843
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation about this study" })
1844
+ ] }),
1845
+ messages.map((message) => /* @__PURE__ */ jsxRuntime.jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsxRuntime.jsx(
1846
+ MessageBubble,
1847
+ {
1848
+ message,
1849
+ isOwn: message.senderId === currentUserId,
1850
+ participants,
1851
+ currentUserId,
1852
+ showSeenBy,
1853
+ onDeepLinkClick,
1854
+ onReply,
1855
+ onPin,
1856
+ onUnpin,
1857
+ onCopy,
1858
+ pinDisabled
1859
+ }
1860
+ ) }, message.id)),
1861
+ typingUsers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles6.typingIndicator, children: [
1862
+ typingUsers.map((t) => t.userName ?? "Someone").join(", "),
1863
+ typingUsers.length === 1 ? " is " : " are ",
1864
+ "typing..."
1865
+ ] }),
1866
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: bottomRef })
1867
+ ]
1868
+ }
1869
+ );
1870
+ });
1871
+ function isNearBottom(container) {
1872
+ if (!container) return true;
1873
+ const threshold = 150;
1874
+ return container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
1875
+ }
1876
+ var styles6 = {
1877
+ container: {
1878
+ flex: 1,
1879
+ overflowY: "auto",
1880
+ paddingTop: "12px",
1881
+ paddingBottom: "8px"
1882
+ },
1883
+ loadingMore: {
1884
+ textAlign: "center",
1885
+ padding: "12px",
1886
+ fontSize: "13px",
1887
+ color: "#9ca3af"
1888
+ },
1889
+ loadMoreButton: {
1890
+ display: "block",
1891
+ margin: "0 auto 12px",
1892
+ padding: "6px 16px",
1893
+ fontSize: "12px",
1894
+ color: "#6b7280",
1895
+ backgroundColor: "transparent",
1896
+ border: "1px solid #e5e7eb",
1897
+ borderRadius: "16px",
1898
+ cursor: "pointer"
1899
+ },
1900
+ empty: {
1901
+ display: "flex",
1902
+ flexDirection: "column",
1903
+ alignItems: "center",
1904
+ justifyContent: "center",
1905
+ padding: "48px 16px",
1906
+ color: "#9ca3af"
1907
+ },
1908
+ emptyTitle: {
1909
+ fontSize: "15px",
1910
+ fontWeight: 500,
1911
+ margin: "0 0 4px"
1912
+ },
1913
+ emptySubtitle: {
1914
+ fontSize: "13px",
1915
+ margin: 0
1916
+ },
1917
+ typingIndicator: {
1918
+ padding: "4px 16px 8px",
1919
+ fontSize: "12px",
1920
+ color: "#9ca3af",
1921
+ fontStyle: "italic"
1922
+ }
1923
+ };
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
+ function ReplyPreview({ message, onCancel, className }) {
2026
+ const preview = renderQuotedPreview(message);
2027
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles7.container, children: [
2028
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles7.bar }),
2029
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles7.content, children: [
2030
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles7.senderLine, children: [
2031
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles7.senderIcon, children: "\u21A9" }),
2032
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles7.senderName, children: [
2033
+ "Replying to ",
2034
+ message.senderName
2035
+ ] })
2036
+ ] }),
2037
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles7.preview, children: preview })
2038
+ ] }),
2039
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onCancel, style: styles7.cancelButton, title: "Cancel reply", type: "button", children: "\u2715" })
2040
+ ] });
2041
+ }
2042
+ function renderQuotedPreview(message) {
2043
+ switch (message.type) {
2044
+ case "audio":
2045
+ return "\u{1F3A4} Voice message";
2046
+ case "image":
2047
+ return `\u{1F4F7} ${message.fileName || "Image"}`;
2048
+ case "file":
2049
+ return `\u{1F4CE} ${message.fileName || "File"}`;
2050
+ case "deep_link":
2051
+ return `\u{1F517} ${message.body}`;
2052
+ default: {
2053
+ const body = message.body || "";
2054
+ return body.length > 120 ? `${body.slice(0, 120)}\u2026` : body;
2055
+ }
2056
+ }
2057
+ }
2058
+ var styles7 = {
2059
+ container: {
2060
+ display: "flex",
2061
+ alignItems: "stretch",
2062
+ gap: "8px",
2063
+ padding: "8px 12px",
2064
+ backgroundColor: "#f9fafb",
2065
+ borderTop: "1px solid #e5e7eb"
2066
+ },
2067
+ bar: {
2068
+ width: "3px",
2069
+ backgroundColor: "#2563eb",
2070
+ borderRadius: "2px",
2071
+ flexShrink: 0
2072
+ },
2073
+ content: {
2074
+ flex: 1,
2075
+ display: "flex",
2076
+ flexDirection: "column",
2077
+ gap: "2px",
2078
+ minWidth: 0
2079
+ },
2080
+ senderLine: {
2081
+ display: "flex",
2082
+ alignItems: "center",
2083
+ gap: "4px"
2084
+ },
2085
+ senderIcon: {
2086
+ fontSize: "11px",
2087
+ color: "#2563eb"
2088
+ },
2089
+ senderName: {
2090
+ fontSize: "12px",
2091
+ fontWeight: 600,
2092
+ color: "#2563eb"
2093
+ },
2094
+ preview: {
2095
+ fontSize: "12px",
2096
+ color: "#6b7280",
2097
+ overflow: "hidden",
2098
+ textOverflow: "ellipsis",
2099
+ whiteSpace: "nowrap"
2100
+ },
2101
+ cancelButton: {
2102
+ alignSelf: "center",
2103
+ width: "24px",
2104
+ height: "24px",
2105
+ display: "flex",
2106
+ alignItems: "center",
2107
+ justifyContent: "center",
2108
+ color: "#6b7280",
2109
+ backgroundColor: "transparent",
2110
+ border: "none",
2111
+ borderRadius: "4px",
2112
+ cursor: "pointer",
2113
+ fontSize: "12px",
2114
+ flexShrink: 0
2115
+ }
2116
+ };
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
+ function MessageInput({
2132
+ onSendText,
2133
+ onSendAudio,
2134
+ onSendFile,
2135
+ onTyping,
2136
+ replyTo,
2137
+ onCancelReply,
2138
+ disabled = false,
2139
+ placeholder = "Type a message...",
2140
+ className
2141
+ }) {
2142
+ const [text, setText] = react.useState("");
2143
+ const [isSending, setIsSending] = react.useState(false);
2144
+ const [fileError, setFileError] = react.useState(null);
2145
+ const fileInputRef = react.useRef(null);
2146
+ const typingTimeoutRef = react.useRef(null);
2147
+ const isTypingRef = react.useRef(false);
2148
+ const sendingRef = react.useRef(false);
2149
+ const { isRecording, duration, start: startRecording, stop: stopRecording, cancel: cancelRecording, isSupported: micSupported, error: micError } = useAudioRecorder();
2150
+ const handleTyping = react.useCallback(() => {
2151
+ if (!isTypingRef.current) {
2152
+ isTypingRef.current = true;
2153
+ onTyping(true);
2154
+ }
2155
+ if (typingTimeoutRef.current) {
2156
+ clearTimeout(typingTimeoutRef.current);
2157
+ }
2158
+ typingTimeoutRef.current = setTimeout(() => {
2159
+ isTypingRef.current = false;
2160
+ onTyping(false);
2161
+ }, 2e3);
2162
+ }, [onTyping]);
2163
+ react.useEffect(() => {
2164
+ return () => {
2165
+ if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
2166
+ };
2167
+ }, []);
2168
+ const handleSendText = react.useCallback(async () => {
2169
+ const trimmed = text.trim();
2170
+ if (!trimmed || sendingRef.current) return;
2171
+ sendingRef.current = true;
2172
+ setIsSending(true);
2173
+ if (isTypingRef.current) {
2174
+ isTypingRef.current = false;
2175
+ onTyping(false);
2176
+ }
2177
+ if (typingTimeoutRef.current) {
2178
+ clearTimeout(typingTimeoutRef.current);
2179
+ typingTimeoutRef.current = null;
2180
+ }
2181
+ try {
2182
+ await onSendText(trimmed);
2183
+ setText("");
2184
+ } finally {
2185
+ sendingRef.current = false;
2186
+ setIsSending(false);
2187
+ }
2188
+ }, [text, onSendText, onTyping]);
2189
+ const handleKeyDown = react.useCallback(
2190
+ (e) => {
2191
+ if (e.key === "Enter" && !e.shiftKey) {
2192
+ e.preventDefault();
2193
+ handleSendText();
2194
+ }
2195
+ },
2196
+ [handleSendText]
2197
+ );
2198
+ const handleAudioStop = react.useCallback(async () => {
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(
2210
+ async (e) => {
2211
+ const file = e.target.files?.[0];
2212
+ if (!file) return;
2213
+ setFileError(null);
2214
+ if (file.size > MAX_FILE_SIZE) {
2215
+ setFileError(`File too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)}MB.`);
2216
+ return;
2217
+ }
2218
+ setIsSending(true);
2219
+ try {
2220
+ await onSendFile(file);
2221
+ } finally {
2222
+ setIsSending(false);
2223
+ if (fileInputRef.current) fileInputRef.current.value = "";
2224
+ }
2225
+ },
2226
+ [onSendFile]
2227
+ );
2228
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles8.container, children: [
2229
+ replyTo && onCancelReply && /* @__PURE__ */ jsxRuntime.jsx(ReplyPreview, { message: replyTo, onCancel: onCancelReply }),
2230
+ (fileError || micError) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles8.error, children: fileError || micError }),
2231
+ isRecording ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingBar, children: [
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
+ ),
2253
+ /* @__PURE__ */ jsxRuntime.jsx(
2254
+ "button",
2255
+ {
2256
+ onClick: () => fileInputRef.current?.click(),
2257
+ disabled: disabled || isSending,
2258
+ style: styles8.iconButton,
2259
+ title: "Attach file",
2260
+ type: "button",
2261
+ children: "\u{1F4CE}"
2262
+ }
2263
+ ),
2264
+ /* @__PURE__ */ jsxRuntime.jsx(
2265
+ "input",
2266
+ {
2267
+ ref: fileInputRef,
2268
+ type: "file",
2269
+ onChange: handleFileSelect,
2270
+ style: { display: "none" },
2271
+ accept: [...SUPPORTED_IMAGE_TYPES, "application/pdf", ".doc", ".docx"].join(",")
2272
+ }
2273
+ ),
2274
+ /* @__PURE__ */ jsxRuntime.jsx(
2275
+ "textarea",
2276
+ {
2277
+ value: text,
2278
+ onChange: (e) => {
2279
+ setText(e.target.value);
2280
+ handleTyping();
2281
+ },
2282
+ onKeyDown: handleKeyDown,
2283
+ placeholder,
2284
+ disabled: disabled || isSending,
2285
+ rows: 1,
2286
+ style: styles8.textarea
2287
+ }
2288
+ ),
2289
+ /* @__PURE__ */ jsxRuntime.jsx(
2290
+ "button",
2291
+ {
2292
+ onClick: handleSendText,
2293
+ disabled: disabled || isSending || !text.trim(),
2294
+ style: {
2295
+ ...styles8.sendButton,
2296
+ opacity: text.trim() ? 1 : 0.4
2297
+ },
2298
+ type: "button",
2299
+ children: "\u27A4"
2300
+ }
2301
+ )
2302
+ ] })
2303
+ ] });
2304
+ }
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
+ var styles8 = {
2311
+ container: {
2312
+ borderTop: "1px solid #e5e7eb",
2313
+ backgroundColor: "#ffffff"
2314
+ },
2315
+ error: {
2316
+ padding: "6px 16px",
2317
+ fontSize: "12px",
2318
+ color: "#dc2626",
2319
+ backgroundColor: "#fef2f2"
2320
+ },
2321
+ inputBar: {
2322
+ display: "flex",
2323
+ alignItems: "flex-end",
2324
+ gap: "4px",
2325
+ padding: "8px 12px"
2326
+ },
2327
+ iconButton: {
2328
+ width: "36px",
2329
+ height: "36px",
2330
+ display: "flex",
2331
+ alignItems: "center",
2332
+ justifyContent: "center",
2333
+ fontSize: "18px",
2334
+ backgroundColor: "transparent",
2335
+ border: "none",
2336
+ borderRadius: "50%",
2337
+ cursor: "pointer",
2338
+ flexShrink: 0
2339
+ },
2340
+ textarea: {
2341
+ flex: 1,
2342
+ padding: "8px 12px",
2343
+ fontSize: "14px",
2344
+ lineHeight: "1.4",
2345
+ border: "1px solid #e5e7eb",
2346
+ borderRadius: "20px",
2347
+ resize: "none",
2348
+ outline: "none",
2349
+ fontFamily: "inherit",
2350
+ maxHeight: "120px"
2351
+ },
2352
+ sendButton: {
2353
+ width: "36px",
2354
+ height: "36px",
2355
+ display: "flex",
2356
+ alignItems: "center",
2357
+ justifyContent: "center",
2358
+ fontSize: "18px",
2359
+ backgroundColor: "#2563eb",
2360
+ color: "#ffffff",
2361
+ border: "none",
2362
+ borderRadius: "50%",
2363
+ cursor: "pointer",
2364
+ 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
+ };
2412
+ function ParticipantsList({
2413
+ participants,
2414
+ currentUserId,
2415
+ isAdmin,
2416
+ onRemoveUser,
2417
+ className
2418
+ }) {
2419
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.item, children: [
2420
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.avatar, children: [
2421
+ participant.avatar ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles9.avatarFallback, children: (participant.userName ?? "?").charAt(0).toUpperCase() }),
2422
+ /* @__PURE__ */ jsxRuntime.jsx(
2423
+ "span",
2424
+ {
2425
+ style: {
2426
+ ...styles9.statusDot,
2427
+ backgroundColor: participant.isOnline ? "#22c55e" : "#d1d5db"
2428
+ }
2429
+ }
2430
+ )
2431
+ ] }),
2432
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.info, children: [
2433
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles9.name, children: [
2434
+ participant.userName ?? fallbackName(participant.userRole),
2435
+ participant.userId === currentUserId && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles9.youBadge, children: " (you)" })
2436
+ ] }),
2437
+ /* @__PURE__ */ jsxRuntime.jsx(RoleBadge2, { role: participant.userRole })
2438
+ ] }),
2439
+ isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsxRuntime.jsx(
2440
+ "button",
2441
+ {
2442
+ onClick: () => onRemoveUser(participant.userId),
2443
+ style: styles9.removeButton,
2444
+ title: `Remove ${participant.userName ?? fallbackName(participant.userRole)}`,
2445
+ type: "button",
2446
+ children: "\u2715"
2447
+ }
2448
+ )
2449
+ ] }, participant.userId)) });
2450
+ }
2451
+ function fallbackName(role) {
2452
+ switch (role) {
2453
+ case "lab":
2454
+ return "Imaging Center";
2455
+ case "radiologist":
2456
+ return "Radiologist";
2457
+ case "physician":
2458
+ return "Physician";
2459
+ case "admin":
2460
+ return "Natoe Admin";
2461
+ default:
2462
+ return "User";
2463
+ }
2464
+ }
2465
+ function RoleBadge2({ role }) {
2466
+ const colors = {
2467
+ radiologist: { bg: "#f5f3ff", text: "#7c3aed" },
2468
+ lab: { bg: "#eff6ff", text: "#2563eb" },
2469
+ physician: { bg: "#ecfdf5", text: "#059669" },
2470
+ admin: { bg: "#fef2f2", text: "#dc2626" }
2471
+ };
2472
+ const color = colors[role];
2473
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles9.roleBadge, backgroundColor: color.bg, color: color.text }, children: role });
2474
+ }
2475
+ var styles9 = {
2476
+ list: {
2477
+ display: "flex",
2478
+ flexDirection: "column"
2479
+ },
2480
+ item: {
2481
+ display: "flex",
2482
+ alignItems: "center",
2483
+ gap: "10px",
2484
+ padding: "8px 0",
2485
+ borderBottom: "1px solid #f9fafb"
2486
+ },
2487
+ avatar: {
2488
+ position: "relative",
2489
+ width: "32px",
2490
+ height: "32px",
2491
+ flexShrink: 0
2492
+ },
2493
+ avatarImg: {
2494
+ width: "32px",
2495
+ height: "32px",
2496
+ borderRadius: "50%",
2497
+ objectFit: "cover"
2498
+ },
2499
+ avatarFallback: {
2500
+ width: "32px",
2501
+ height: "32px",
2502
+ borderRadius: "50%",
2503
+ backgroundColor: "#e5e7eb",
2504
+ display: "flex",
2505
+ alignItems: "center",
2506
+ justifyContent: "center",
2507
+ fontSize: "13px",
2508
+ fontWeight: 600,
2509
+ color: "#6b7280"
2510
+ },
2511
+ statusDot: {
2512
+ position: "absolute",
2513
+ bottom: "0",
2514
+ right: "0",
2515
+ width: "8px",
2516
+ height: "8px",
2517
+ borderRadius: "50%",
2518
+ border: "2px solid #ffffff"
2519
+ },
2520
+ info: {
2521
+ display: "flex",
2522
+ alignItems: "center",
2523
+ gap: "6px",
2524
+ flex: 1,
2525
+ minWidth: 0
2526
+ },
2527
+ name: {
2528
+ fontSize: "13px",
2529
+ fontWeight: 500,
2530
+ color: "#111827",
2531
+ overflow: "hidden",
2532
+ textOverflow: "ellipsis",
2533
+ whiteSpace: "nowrap"
2534
+ },
2535
+ youBadge: {
2536
+ fontSize: "11px",
2537
+ fontWeight: 400,
2538
+ color: "#9ca3af"
2539
+ },
2540
+ roleBadge: {
2541
+ fontSize: "10px",
2542
+ fontWeight: 500,
2543
+ padding: "1px 6px",
2544
+ borderRadius: "4px",
2545
+ whiteSpace: "nowrap"
2546
+ },
2547
+ removeButton: {
2548
+ width: "24px",
2549
+ height: "24px",
2550
+ display: "flex",
2551
+ alignItems: "center",
2552
+ justifyContent: "center",
2553
+ backgroundColor: "transparent",
2554
+ border: "none",
2555
+ borderRadius: "4px",
2556
+ cursor: "pointer",
2557
+ fontSize: "11px",
2558
+ color: "#9ca3af",
2559
+ flexShrink: 0
2560
+ }
2561
+ };
2562
+ function ChannelSettings({
2563
+ channelName,
2564
+ participants,
2565
+ currentUserId,
2566
+ isAdmin,
2567
+ isUpdating,
2568
+ error,
2569
+ onUpdateChannel,
2570
+ onInviteUser,
2571
+ onRemoveUser,
2572
+ onLeaveChannel,
2573
+ onDeleteChannel,
2574
+ onClose,
2575
+ className
2576
+ }) {
2577
+ const [editingName, setEditingName] = react.useState(false);
2578
+ const [nameValue, setNameValue] = react.useState(channelName);
2579
+ const [showInvite, setShowInvite] = react.useState(false);
2580
+ const [inviteId, setInviteId] = react.useState("");
2581
+ const [inviteName, setInviteName] = react.useState("");
2582
+ const [inviteRole, setInviteRole] = react.useState("radiologist");
2583
+ const [confirmDelete, setConfirmDelete] = react.useState(false);
2584
+ const handleSaveName = async () => {
2585
+ if (nameValue.trim() && nameValue !== channelName) {
2586
+ await onUpdateChannel({ name: nameValue.trim() });
2587
+ }
2588
+ setEditingName(false);
2589
+ };
2590
+ const handleInvite = async () => {
2591
+ if (!inviteId.trim() || !inviteName.trim()) return;
2592
+ await onInviteUser({
2593
+ userId: inviteId.trim(),
2594
+ userName: inviteName.trim(),
2595
+ userRole: inviteRole
2596
+ });
2597
+ setInviteId("");
2598
+ setInviteName("");
2599
+ setShowInvite(false);
2600
+ };
2601
+ const handleDelete = async () => {
2602
+ if (!confirmDelete) {
2603
+ setConfirmDelete(true);
2604
+ return;
2605
+ }
2606
+ await onDeleteChannel();
2607
+ };
2608
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles10.container, children: [
2609
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.header, children: [
2610
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: styles10.title, children: "Channel Settings" }),
2611
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClose, style: styles10.closeButton, type: "button", children: "\u2715" })
2612
+ ] }),
2613
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles10.error, children: error.message }),
2614
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.section, children: [
2615
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: styles10.label, children: "Channel Name" }),
2616
+ editingName ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.editRow, children: [
2617
+ /* @__PURE__ */ jsxRuntime.jsx(
2618
+ "input",
2619
+ {
2620
+ value: nameValue,
2621
+ onChange: (e) => setNameValue(e.target.value),
2622
+ style: styles10.input,
2623
+ autoFocus: true
2624
+ }
2625
+ ),
2626
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleSaveName, disabled: isUpdating, style: styles10.saveButton, type: "button", children: "Save" }),
2627
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setEditingName(false), style: styles10.cancelButton, type: "button", children: "Cancel" })
2628
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.editRow, children: [
2629
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles10.channelName, children: channelName }),
2630
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setEditingName(true), style: styles10.editButton, type: "button", children: "Edit" })
2631
+ ] })
2632
+ ] }),
2633
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.section, children: [
2634
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.sectionHeader, children: [
2635
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: styles10.label, children: [
2636
+ "Participants (",
2637
+ participants.length,
2638
+ ")"
2639
+ ] }),
2640
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setShowInvite(!showInvite), style: styles10.addButton, type: "button", children: "+ Add" })
2641
+ ] }),
2642
+ showInvite && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.inviteForm, children: [
2643
+ /* @__PURE__ */ jsxRuntime.jsx(
2644
+ "input",
2645
+ {
2646
+ value: inviteId,
2647
+ onChange: (e) => setInviteId(e.target.value),
2648
+ placeholder: "User ID",
2649
+ style: styles10.input
2650
+ }
2651
+ ),
2652
+ /* @__PURE__ */ jsxRuntime.jsx(
2653
+ "input",
2654
+ {
2655
+ value: inviteName,
2656
+ onChange: (e) => setInviteName(e.target.value),
2657
+ placeholder: "User Name",
2658
+ style: styles10.input
2659
+ }
2660
+ ),
2661
+ /* @__PURE__ */ jsxRuntime.jsxs(
2662
+ "select",
2663
+ {
2664
+ value: inviteRole,
2665
+ onChange: (e) => setInviteRole(e.target.value),
2666
+ style: styles10.input,
2667
+ children: [
2668
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "radiologist", children: "Radiologist" }),
2669
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "lab", children: "Lab" }),
2670
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "physician", children: "Physician" }),
2671
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "admin", children: "Admin" })
2672
+ ]
2673
+ }
2674
+ ),
2675
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.inviteActions, children: [
2676
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleInvite, disabled: isUpdating, style: styles10.saveButton, type: "button", children: "Invite" }),
2677
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setShowInvite(false), style: styles10.cancelButton, type: "button", children: "Cancel" })
2678
+ ] })
2679
+ ] }),
2680
+ /* @__PURE__ */ jsxRuntime.jsx(
2681
+ ParticipantsList,
2682
+ {
2683
+ participants,
2684
+ currentUserId,
2685
+ isAdmin,
2686
+ onRemoveUser
2687
+ }
2688
+ )
2689
+ ] }),
2690
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles10.dangerZone, children: [
2691
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onLeaveChannel, disabled: isUpdating, style: styles10.leaveButton, type: "button", children: "Leave Channel" }),
2692
+ isAdmin && /* @__PURE__ */ jsxRuntime.jsx(
2693
+ "button",
2694
+ {
2695
+ onClick: handleDelete,
2696
+ disabled: isUpdating,
2697
+ style: confirmDelete ? styles10.confirmDeleteButton : styles10.deleteButton,
2698
+ type: "button",
2699
+ children: confirmDelete ? "Confirm Delete" : "Delete Channel"
2700
+ }
2701
+ )
2702
+ ] })
2703
+ ] });
2704
+ }
2705
+ var styles10 = {
2706
+ container: {
2707
+ display: "flex",
2708
+ flexDirection: "column",
2709
+ height: "100%",
2710
+ backgroundColor: "#ffffff"
2711
+ },
2712
+ header: {
2713
+ display: "flex",
2714
+ justifyContent: "space-between",
2715
+ alignItems: "center",
2716
+ padding: "12px 16px",
2717
+ borderBottom: "1px solid #e5e7eb"
2718
+ },
2719
+ title: {
2720
+ margin: 0,
2721
+ fontSize: "16px",
2722
+ fontWeight: 600
2723
+ },
2724
+ closeButton: {
2725
+ width: "28px",
2726
+ height: "28px",
2727
+ display: "flex",
2728
+ alignItems: "center",
2729
+ justifyContent: "center",
2730
+ backgroundColor: "transparent",
2731
+ border: "none",
2732
+ borderRadius: "4px",
2733
+ cursor: "pointer",
2734
+ fontSize: "14px",
2735
+ color: "#6b7280"
2736
+ },
2737
+ error: {
2738
+ padding: "8px 16px",
2739
+ fontSize: "13px",
2740
+ color: "#dc2626",
2741
+ backgroundColor: "#fef2f2"
2742
+ },
2743
+ section: {
2744
+ padding: "16px",
2745
+ borderBottom: "1px solid #f3f4f6"
2746
+ },
2747
+ sectionHeader: {
2748
+ display: "flex",
2749
+ justifyContent: "space-between",
2750
+ alignItems: "center",
2751
+ marginBottom: "8px"
2752
+ },
2753
+ label: {
2754
+ fontSize: "12px",
2755
+ fontWeight: 600,
2756
+ color: "#6b7280",
2757
+ textTransform: "uppercase",
2758
+ letterSpacing: "0.05em"
2759
+ },
2760
+ channelName: {
2761
+ fontSize: "14px",
2762
+ color: "#111827"
2763
+ },
2764
+ editRow: {
2765
+ display: "flex",
2766
+ alignItems: "center",
2767
+ gap: "8px",
2768
+ marginTop: "6px"
2769
+ },
2770
+ input: {
2771
+ padding: "6px 10px",
2772
+ fontSize: "13px",
2773
+ border: "1px solid #d1d5db",
2774
+ borderRadius: "6px",
2775
+ outline: "none",
2776
+ width: "100%"
2777
+ },
2778
+ editButton: {
2779
+ padding: "4px 10px",
2780
+ fontSize: "12px",
2781
+ color: "#2563eb",
2782
+ backgroundColor: "transparent",
2783
+ border: "none",
2784
+ cursor: "pointer"
2785
+ },
2786
+ saveButton: {
2787
+ padding: "6px 12px",
2788
+ fontSize: "12px",
2789
+ fontWeight: 500,
2790
+ color: "#ffffff",
2791
+ backgroundColor: "#2563eb",
2792
+ border: "none",
2793
+ borderRadius: "6px",
2794
+ cursor: "pointer",
2795
+ whiteSpace: "nowrap"
2796
+ },
2797
+ cancelButton: {
2798
+ padding: "6px 12px",
2799
+ fontSize: "12px",
2800
+ color: "#6b7280",
2801
+ backgroundColor: "transparent",
2802
+ border: "1px solid #d1d5db",
2803
+ borderRadius: "6px",
2804
+ cursor: "pointer",
2805
+ whiteSpace: "nowrap"
2806
+ },
2807
+ addButton: {
2808
+ padding: "4px 10px",
2809
+ fontSize: "12px",
2810
+ fontWeight: 500,
2811
+ color: "#2563eb",
2812
+ backgroundColor: "#eff6ff",
2813
+ border: "none",
2814
+ borderRadius: "4px",
2815
+ cursor: "pointer"
2816
+ },
2817
+ inviteForm: {
2818
+ display: "flex",
2819
+ flexDirection: "column",
2820
+ gap: "8px",
2821
+ padding: "12px",
2822
+ marginBottom: "12px",
2823
+ backgroundColor: "#f9fafb",
2824
+ borderRadius: "8px"
2825
+ },
2826
+ inviteActions: {
2827
+ display: "flex",
2828
+ gap: "8px"
2829
+ },
2830
+ dangerZone: {
2831
+ padding: "16px",
2832
+ marginTop: "auto",
2833
+ display: "flex",
2834
+ flexDirection: "column",
2835
+ gap: "8px"
2836
+ },
2837
+ leaveButton: {
2838
+ padding: "8px",
2839
+ fontSize: "13px",
2840
+ fontWeight: 500,
2841
+ color: "#b45309",
2842
+ backgroundColor: "#fffbeb",
2843
+ border: "1px solid #fde68a",
2844
+ borderRadius: "6px",
2845
+ cursor: "pointer"
2846
+ },
2847
+ deleteButton: {
2848
+ padding: "8px",
2849
+ fontSize: "13px",
2850
+ fontWeight: 500,
2851
+ color: "#dc2626",
2852
+ backgroundColor: "#fef2f2",
2853
+ border: "1px solid #fecaca",
2854
+ borderRadius: "6px",
2855
+ cursor: "pointer"
2856
+ },
2857
+ confirmDeleteButton: {
2858
+ padding: "8px",
2859
+ fontSize: "13px",
2860
+ fontWeight: 600,
2861
+ color: "#ffffff",
2862
+ backgroundColor: "#dc2626",
2863
+ border: "none",
2864
+ borderRadius: "6px",
2865
+ cursor: "pointer"
2866
+ }
2867
+ };
2868
+ function PinnedMessagesBar({
2869
+ pinnedMessages,
2870
+ onJumpTo,
2871
+ onUnpin,
2872
+ className
2873
+ }) {
2874
+ const [index, setIndex] = react.useState(0);
2875
+ if (pinnedMessages.length === 0) return null;
2876
+ const safeIndex = Math.min(index, pinnedMessages.length - 1);
2877
+ const current = pinnedMessages[safeIndex];
2878
+ const preview = renderPinPreview(current);
2879
+ const handleClick = () => {
2880
+ onJumpTo(current.id);
2881
+ if (pinnedMessages.length > 1) {
2882
+ setIndex((prev) => (prev + 1) % pinnedMessages.length);
2883
+ }
2884
+ };
2885
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles11.container, children: [
2886
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles11.indicator, children: [
2887
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles11.pinIcon, children: "\u{1F4CC}" }),
2888
+ pinnedMessages.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles11.segments, children: pinnedMessages.map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
2889
+ "span",
2890
+ {
2891
+ style: {
2892
+ ...styles11.segment,
2893
+ backgroundColor: i === safeIndex ? "#2563eb" : "#d1d5db"
2894
+ }
2895
+ },
2896
+ i
2897
+ )) })
2898
+ ] }),
2899
+ /* @__PURE__ */ jsxRuntime.jsxs(
2900
+ "button",
2901
+ {
2902
+ onClick: handleClick,
2903
+ style: styles11.contentButton,
2904
+ type: "button",
2905
+ title: "Click to jump to message",
2906
+ children: [
2907
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles11.label, children: [
2908
+ "Pinned ",
2909
+ pinnedMessages.length > 1 ? `(${safeIndex + 1}/${pinnedMessages.length})` : ""
2910
+ ] }),
2911
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles11.preview, children: [
2912
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles11.sender, children: [
2913
+ current.senderName,
2914
+ ":"
2915
+ ] }),
2916
+ " ",
2917
+ preview
2918
+ ] })
2919
+ ]
2920
+ }
2921
+ ),
2922
+ /* @__PURE__ */ jsxRuntime.jsx(
2923
+ "button",
2924
+ {
2925
+ onClick: (e) => {
2926
+ e.stopPropagation();
2927
+ onUnpin(current.id);
2928
+ },
2929
+ style: styles11.unpinButton,
2930
+ title: "Unpin this message",
2931
+ type: "button",
2932
+ children: "\u2715"
2933
+ }
2934
+ )
2935
+ ] });
2936
+ }
2937
+ function renderPinPreview(message) {
2938
+ switch (message.type) {
2939
+ case "audio":
2940
+ return "\u{1F3A4} Voice message";
2941
+ case "image":
2942
+ return `\u{1F4F7} ${message.fileName || "Image"}`;
2943
+ case "file":
2944
+ return `\u{1F4CE} ${message.fileName || "File"}`;
2945
+ case "deep_link":
2946
+ return `\u{1F517} ${message.body}`;
2947
+ default: {
2948
+ const body = message.body || "";
2949
+ return body.length > 100 ? `${body.slice(0, 100)}\u2026` : body;
2950
+ }
2951
+ }
2952
+ }
2953
+ var styles11 = {
2954
+ container: {
2955
+ display: "flex",
2956
+ alignItems: "center",
2957
+ gap: "8px",
2958
+ padding: "6px 12px",
2959
+ backgroundColor: "#eff6ff",
2960
+ borderBottom: "1px solid #dbeafe"
2961
+ },
2962
+ indicator: {
2963
+ display: "flex",
2964
+ alignItems: "center",
2965
+ gap: "4px",
2966
+ flexShrink: 0
2967
+ },
2968
+ pinIcon: {
2969
+ fontSize: "13px"
2970
+ },
2971
+ segments: {
2972
+ display: "flex",
2973
+ flexDirection: "column",
2974
+ gap: "2px"
2975
+ },
2976
+ segment: {
2977
+ width: "3px",
2978
+ height: "6px",
2979
+ borderRadius: "1px"
2980
+ },
2981
+ contentButton: {
2982
+ flex: 1,
2983
+ display: "flex",
2984
+ flexDirection: "column",
2985
+ gap: "2px",
2986
+ padding: "2px 0",
2987
+ backgroundColor: "transparent",
2988
+ border: "none",
2989
+ textAlign: "left",
2990
+ cursor: "pointer",
2991
+ minWidth: 0,
2992
+ overflow: "hidden"
2993
+ },
2994
+ label: {
2995
+ fontSize: "11px",
2996
+ fontWeight: 600,
2997
+ color: "#2563eb"
2998
+ },
2999
+ preview: {
3000
+ fontSize: "12px",
3001
+ color: "#374151",
3002
+ overflow: "hidden",
3003
+ textOverflow: "ellipsis",
3004
+ whiteSpace: "nowrap"
3005
+ },
3006
+ sender: {
3007
+ fontWeight: 600
3008
+ },
3009
+ unpinButton: {
3010
+ width: "24px",
3011
+ height: "24px",
3012
+ display: "flex",
3013
+ alignItems: "center",
3014
+ justifyContent: "center",
3015
+ color: "#6b7280",
3016
+ backgroundColor: "transparent",
3017
+ border: "none",
3018
+ borderRadius: "4px",
3019
+ cursor: "pointer",
3020
+ fontSize: "11px",
3021
+ flexShrink: 0
3022
+ }
3023
+ };
3024
+ function CollabPanel({
3025
+ orderId,
3026
+ patientData,
3027
+ participantIds,
3028
+ showSeenBy = true,
3029
+ className,
3030
+ style
3031
+ }) {
3032
+ const { config } = useCollab();
3033
+ const [showSettings, setShowSettings] = react.useState(false);
3034
+ const messageListRef = react.useRef(null);
3035
+ const {
3036
+ conversation,
3037
+ messages,
3038
+ pinnedMessages,
3039
+ participants,
3040
+ typingUsers,
3041
+ isLoading,
3042
+ hasMore,
3043
+ error,
3044
+ replyTo,
3045
+ setReplyTo,
3046
+ sendMessage,
3047
+ sendAudioMessage,
3048
+ sendFileMessage,
3049
+ sendTyping,
3050
+ markAsRead,
3051
+ loadMoreMessages,
3052
+ pinMessage,
3053
+ unpinMessage
3054
+ } = useConversation({ orderId, patientData, participantIds });
3055
+ const channelSettings = useChannelSettings({
3056
+ conversationId: conversation?.id ?? null
3057
+ });
3058
+ const { handleDeepLink } = useDeepLinks();
3059
+ const handleOpenDicom = () => {
3060
+ if (patientData.studyId && patientData.storageId && config.onOpenDicom) {
3061
+ config.onOpenDicom(patientData.studyId, patientData.storageId);
3062
+ }
3063
+ };
3064
+ const handleJumpToMessage = (messageId) => {
3065
+ messageListRef.current?.scrollToMessage(messageId);
3066
+ };
3067
+ const handleCopy = async (text) => {
3068
+ try {
3069
+ if (navigator.clipboard) {
3070
+ await navigator.clipboard.writeText(text);
3071
+ }
3072
+ } catch {
3073
+ }
3074
+ };
3075
+ const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
3076
+ if (error) {
3077
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyles.errorState, children: [
3078
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
3079
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: panelStyles.errorMessage, children: error })
3080
+ ] }) });
3081
+ }
3082
+ if (isLoading && !conversation) {
3083
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3084
+ }
3085
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: showSettings && conversation ? /* @__PURE__ */ jsxRuntime.jsx(
3086
+ ChannelSettings,
3087
+ {
3088
+ conversationId: conversation.id,
3089
+ channelName: conversation.name,
3090
+ channelPicture: conversation.picture,
3091
+ participants,
3092
+ currentUserId: config.userId,
3093
+ isAdmin: channelSettings.isAdmin,
3094
+ isUpdating: channelSettings.isUpdating,
3095
+ error: channelSettings.error,
3096
+ onUpdateChannel: channelSettings.updateChannel,
3097
+ onInviteUser: channelSettings.inviteUser,
3098
+ onRemoveUser: channelSettings.removeUser,
3099
+ onLeaveChannel: channelSettings.leaveChannel,
3100
+ onDeleteChannel: channelSettings.deleteChannel,
3101
+ onClose: () => setShowSettings(false)
3102
+ }
3103
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3104
+ /* @__PURE__ */ jsxRuntime.jsx(
3105
+ PatientHeader,
3106
+ {
3107
+ patientData,
3108
+ participants,
3109
+ onOpenDicom: handleOpenDicom,
3110
+ onOpenSettings: () => setShowSettings(true)
3111
+ }
3112
+ ),
3113
+ pinnedMessages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3114
+ PinnedMessagesBar,
3115
+ {
3116
+ pinnedMessages,
3117
+ onJumpTo: handleJumpToMessage,
3118
+ onUnpin: unpinMessage
3119
+ }
3120
+ ),
3121
+ /* @__PURE__ */ jsxRuntime.jsx(
3122
+ MessageList,
3123
+ {
3124
+ ref: messageListRef,
3125
+ messages,
3126
+ currentUserId: config.userId,
3127
+ participants,
3128
+ showSeenBy,
3129
+ typingUsers,
3130
+ hasMore,
3131
+ isLoading,
3132
+ onLoadMore: loadMoreMessages,
3133
+ onDeepLinkClick: handleDeepLink,
3134
+ onMessageVisible: markAsRead,
3135
+ onReply: setReplyTo,
3136
+ onPin: pinMessage,
3137
+ onUnpin: unpinMessage,
3138
+ onCopy: handleCopy,
3139
+ pinDisabled
3140
+ }
3141
+ ),
3142
+ /* @__PURE__ */ jsxRuntime.jsx(
3143
+ MessageInput,
3144
+ {
3145
+ onSendText: sendMessage,
3146
+ onSendAudio: sendAudioMessage,
3147
+ onSendFile: sendFileMessage,
3148
+ onTyping: sendTyping,
3149
+ replyTo,
3150
+ onCancelReply: () => setReplyTo(null)
3151
+ }
3152
+ )
3153
+ ] }) });
3154
+ }
3155
+ var panelStyles = {
3156
+ container: {
3157
+ display: "flex",
3158
+ flexDirection: "column",
3159
+ height: "100%",
3160
+ backgroundColor: "#ffffff",
3161
+ borderRadius: "8px",
3162
+ overflow: "hidden",
3163
+ border: "1px solid #e5e7eb",
3164
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
3165
+ },
3166
+ loadingState: {
3167
+ display: "flex",
3168
+ alignItems: "center",
3169
+ justifyContent: "center",
3170
+ height: "100%",
3171
+ fontSize: "14px",
3172
+ color: "#9ca3af"
3173
+ },
3174
+ errorState: {
3175
+ display: "flex",
3176
+ flexDirection: "column",
3177
+ alignItems: "center",
3178
+ justifyContent: "center",
3179
+ height: "100%",
3180
+ padding: "24px",
3181
+ textAlign: "center"
3182
+ },
3183
+ errorTitle: {
3184
+ fontSize: "15px",
3185
+ fontWeight: 500,
3186
+ color: "#dc2626",
3187
+ margin: "0 0 4px"
3188
+ },
3189
+ errorMessage: {
3190
+ fontSize: "13px",
3191
+ color: "#6b7280",
3192
+ margin: 0
3193
+ }
3194
+ };
3195
+ function CollabPopup({
3196
+ orderId,
3197
+ patientData,
3198
+ participantIds,
3199
+ isOpen,
3200
+ onClose,
3201
+ initialPosition,
3202
+ width = 380,
3203
+ height = 520,
3204
+ className
3205
+ }) {
3206
+ const [position, setPosition] = react.useState(
3207
+ initialPosition || {
3208
+ x: window.innerWidth - width - 24,
3209
+ y: Math.max(24, window.innerHeight - height - 104)
3210
+ }
3211
+ );
3212
+ const [isDragging, setIsDragging] = react.useState(false);
3213
+ const [isMinimized, setIsMinimized] = react.useState(false);
3214
+ const dragOffset = react.useRef({ x: 0, y: 0 });
3215
+ const containerRef = react.useRef(null);
3216
+ const handleMouseDown = react.useCallback(
3217
+ (e) => {
3218
+ if (!e.target.closest("[data-drag-handle]")) return;
3219
+ setIsDragging(true);
3220
+ dragOffset.current = {
3221
+ x: e.clientX - position.x,
3222
+ y: e.clientY - position.y
3223
+ };
3224
+ e.preventDefault();
3225
+ },
3226
+ [position]
3227
+ );
3228
+ react.useEffect(() => {
3229
+ if (!isOpen) return;
3230
+ const handleKey = (e) => {
3231
+ if (e.key === "Escape") {
3232
+ onClose();
3233
+ }
3234
+ };
3235
+ window.addEventListener("keydown", handleKey);
3236
+ return () => window.removeEventListener("keydown", handleKey);
3237
+ }, [isOpen, onClose]);
3238
+ react.useEffect(() => {
3239
+ if (!isDragging) return;
3240
+ const handleMouseMove = (e) => {
3241
+ setPosition({
3242
+ x: Math.max(0, Math.min(e.clientX - dragOffset.current.x, window.innerWidth - width)),
3243
+ y: Math.max(0, Math.min(e.clientY - dragOffset.current.y, window.innerHeight - (isMinimized ? 48 : height)))
3244
+ });
3245
+ };
3246
+ const handleMouseUp = () => {
3247
+ setIsDragging(false);
3248
+ };
3249
+ document.addEventListener("mousemove", handleMouseMove);
3250
+ document.addEventListener("mouseup", handleMouseUp);
3251
+ return () => {
3252
+ document.removeEventListener("mousemove", handleMouseMove);
3253
+ document.removeEventListener("mouseup", handleMouseUp);
3254
+ };
3255
+ }, [isDragging, width, height, isMinimized]);
3256
+ if (!isOpen) return null;
3257
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3258
+ "div",
3259
+ {
3260
+ ref: containerRef,
3261
+ className,
3262
+ style: {
3263
+ ...styles12.container,
3264
+ left: position.x,
3265
+ top: position.y,
3266
+ width,
3267
+ height: isMinimized ? 48 : height,
3268
+ cursor: isDragging ? "grabbing" : "default"
3269
+ },
3270
+ onMouseDown: handleMouseDown,
3271
+ children: [
3272
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-drag-handle": true, style: styles12.titleBar, children: [
3273
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles12.titleText, children: buildChannelName(patientData) }),
3274
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles12.titleActions, children: [
3275
+ /* @__PURE__ */ jsxRuntime.jsx(
3276
+ "button",
3277
+ {
3278
+ onClick: () => setIsMinimized(!isMinimized),
3279
+ style: styles12.titleButton,
3280
+ title: isMinimized ? "Expand" : "Minimize",
3281
+ type: "button",
3282
+ children: isMinimized ? "\u25A2" : "\u2014"
3283
+ }
3284
+ ),
3285
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClose, style: styles12.titleButton, title: "Close", type: "button", children: "\u2715" })
3286
+ ] })
3287
+ ] }),
3288
+ !isMinimized && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles12.panelWrapper, children: /* @__PURE__ */ jsxRuntime.jsx(CollabPanel, { orderId, patientData, participantIds }) })
3289
+ ]
3290
+ }
3291
+ );
3292
+ }
3293
+ var styles12 = {
3294
+ container: {
3295
+ position: "fixed",
3296
+ zIndex: 9999,
3297
+ borderRadius: "12px",
3298
+ boxShadow: "0 8px 30px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08)",
3299
+ overflow: "hidden",
3300
+ display: "flex",
3301
+ flexDirection: "column",
3302
+ backgroundColor: "#ffffff",
3303
+ border: "1px solid #e5e7eb",
3304
+ transition: "height 0.2s ease"
3305
+ },
3306
+ titleBar: {
3307
+ display: "flex",
3308
+ justifyContent: "space-between",
3309
+ alignItems: "center",
3310
+ padding: "0 12px",
3311
+ height: "48px",
3312
+ backgroundColor: "#1e293b",
3313
+ cursor: "grab",
3314
+ userSelect: "none",
3315
+ flexShrink: 0
3316
+ },
3317
+ titleText: {
3318
+ fontSize: "13px",
3319
+ fontWeight: 500,
3320
+ color: "#ffffff",
3321
+ overflow: "hidden",
3322
+ textOverflow: "ellipsis",
3323
+ whiteSpace: "nowrap"
3324
+ },
3325
+ titleActions: {
3326
+ display: "flex",
3327
+ gap: "4px",
3328
+ flexShrink: 0
3329
+ },
3330
+ titleButton: {
3331
+ width: "28px",
3332
+ height: "28px",
3333
+ display: "flex",
3334
+ alignItems: "center",
3335
+ justifyContent: "center",
3336
+ backgroundColor: "transparent",
3337
+ border: "none",
3338
+ borderRadius: "4px",
3339
+ cursor: "pointer",
3340
+ fontSize: "13px",
3341
+ color: "#94a3b8"
3342
+ },
3343
+ panelWrapper: {
3344
+ flex: 1,
3345
+ overflow: "hidden"
3346
+ }
3347
+ };
3348
+ function useInlineCollab({
3349
+ orderId,
3350
+ patientData,
3351
+ participantIds = [],
3352
+ messageLimit = 5
3353
+ }) {
3354
+ const {
3355
+ socket,
3356
+ config,
3357
+ requestPreview,
3358
+ invalidatePreview,
3359
+ createConversationWithMessage
3360
+ } = useCollab();
3361
+ const [preview, setPreview] = react.useState(null);
3362
+ const [messages, setMessages] = react.useState([]);
3363
+ const [participants, setParticipants] = react.useState([]);
3364
+ const [unreadCount, setUnreadCount] = react.useState(0);
3365
+ const [isLoading, setIsLoading] = react.useState(true);
3366
+ const [isSubscribed, setIsSubscribed] = react.useState(false);
3367
+ const [error, setError] = react.useState(null);
3368
+ const elementRef = react.useRef(null);
3369
+ const observerRef = react.useRef(null);
3370
+ const subscribedConversationIdRef = react.useRef(null);
3371
+ const trimToLimit = react.useCallback(
3372
+ (msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
3373
+ [messageLimit]
3374
+ );
3375
+ const buildName = react.useCallback(
3376
+ () => buildChannelName(patientData),
3377
+ [patientData]
3378
+ );
3379
+ react.useEffect(() => {
3380
+ let cancelled = false;
3381
+ const load = async () => {
3382
+ setIsLoading(true);
3383
+ setError(null);
3384
+ try {
3385
+ const result = await requestPreview(orderId);
3386
+ if (cancelled) return;
3387
+ setPreview(result);
3388
+ if (result) {
3389
+ setMessages(trimToLimit(result.lastMessages));
3390
+ setParticipants(result.participants);
3391
+ setUnreadCount(result.unreadCount);
3392
+ }
3393
+ } catch (err) {
3394
+ if (cancelled) return;
3395
+ setError(err instanceof Error ? err.message : "Failed to load preview");
3396
+ } finally {
3397
+ if (!cancelled) setIsLoading(false);
3398
+ }
3399
+ };
3400
+ load();
3401
+ return () => {
3402
+ cancelled = true;
3403
+ };
3404
+ }, [orderId, requestPreview, trimToLimit]);
3405
+ const subscribe = react.useCallback(
3406
+ (conversationId) => {
3407
+ if (subscribedConversationIdRef.current === conversationId) return;
3408
+ socket.joinConversation(conversationId, {
3409
+ onMessage: (msg) => {
3410
+ setMessages((prev) => trimToLimit([...prev, msg]));
3411
+ if (msg.senderId !== config.userId) {
3412
+ setUnreadCount((prev) => prev + 1);
3413
+ }
3414
+ },
3415
+ onUserJoined: (participant) => {
3416
+ setParticipants(
3417
+ (prev) => prev.some((p) => p.userId === participant.userId) ? prev : [...prev, participant]
3418
+ );
3419
+ },
3420
+ onUserLeft: (participant) => {
3421
+ setParticipants((prev) => prev.filter((p) => p.userId !== participant.userId));
3422
+ }
3423
+ });
3424
+ subscribedConversationIdRef.current = conversationId;
3425
+ setIsSubscribed(true);
3426
+ },
3427
+ [socket, trimToLimit, config.userId]
3428
+ );
3429
+ const unsubscribe = react.useCallback(() => {
3430
+ const convId = subscribedConversationIdRef.current;
3431
+ if (convId) {
3432
+ socket.leaveConversation(convId);
3433
+ subscribedConversationIdRef.current = null;
3434
+ setIsSubscribed(false);
3435
+ }
3436
+ }, [socket]);
3437
+ const containerRef = react.useCallback(
3438
+ (element) => {
3439
+ if (observerRef.current) {
3440
+ observerRef.current.disconnect();
3441
+ observerRef.current = null;
3442
+ }
3443
+ elementRef.current = element;
3444
+ if (!element) return;
3445
+ observerRef.current = new IntersectionObserver(
3446
+ (entries) => {
3447
+ const entry = entries[0];
3448
+ if (!entry) return;
3449
+ const convId = preview?.conversationId;
3450
+ if (!convId) return;
3451
+ if (entry.isIntersecting) {
3452
+ subscribe(convId);
3453
+ } else {
3454
+ unsubscribe();
3455
+ }
3456
+ },
3457
+ { threshold: 0.1 }
3458
+ );
3459
+ observerRef.current.observe(element);
3460
+ },
3461
+ [preview, subscribe, unsubscribe]
3462
+ );
3463
+ react.useEffect(() => {
3464
+ return () => {
3465
+ if (observerRef.current) observerRef.current.disconnect();
3466
+ unsubscribe();
3467
+ };
3468
+ }, [unsubscribe]);
3469
+ react.useEffect(() => {
3470
+ if (preview?.conversationId && elementRef.current && !observerRef.current) {
3471
+ containerRef(elementRef.current);
3472
+ }
3473
+ }, [preview, containerRef]);
3474
+ const dispatchSend = react.useCallback(
3475
+ async (payload) => {
3476
+ if (!preview) {
3477
+ try {
3478
+ const result = await createConversationWithMessage(orderId, payload, {
3479
+ name: buildName(),
3480
+ participantIds
3481
+ });
3482
+ const newPreview = {
3483
+ conversationId: result.conversation.id,
3484
+ orderId,
3485
+ messageCount: 1,
3486
+ unreadCount: 0,
3487
+ lastMessages: [result.message],
3488
+ participants: result.conversation.participants,
3489
+ name: result.conversation.name
3490
+ };
3491
+ setPreview(newPreview);
3492
+ setMessages([result.message]);
3493
+ setParticipants(result.conversation.participants);
3494
+ invalidatePreview(orderId);
3495
+ if (elementRef.current) {
3496
+ subscribe(result.conversation.id);
3497
+ }
3498
+ } catch (err) {
3499
+ setError(err instanceof Error ? err.message : "Failed to send message");
3500
+ config.onError?.({
3501
+ code: "SEND_ERROR",
3502
+ message: "Failed to send first message",
3503
+ details: err
3504
+ });
3505
+ }
3506
+ return;
3507
+ }
3508
+ try {
3509
+ await socket.sendMessage(preview.conversationId, payload);
3510
+ } catch (err) {
3511
+ setError(err instanceof Error ? err.message : "Failed to send message");
3512
+ config.onError?.({
3513
+ code: "SEND_ERROR",
3514
+ message: "Failed to send message",
3515
+ details: err
3516
+ });
3517
+ }
3518
+ },
3519
+ [
3520
+ preview,
3521
+ orderId,
3522
+ buildName,
3523
+ participantIds,
3524
+ createConversationWithMessage,
3525
+ invalidatePreview,
3526
+ subscribe,
3527
+ socket,
3528
+ config
3529
+ ]
3530
+ );
3531
+ const sendMessage = react.useCallback(
3532
+ async (body) => {
3533
+ const trimmed = body.trim();
3534
+ if (!trimmed) return;
3535
+ await dispatchSend({ body: trimmed, type: "text" });
3536
+ },
3537
+ [dispatchSend]
3538
+ );
3539
+ const sendAudioMessage = react.useCallback(
3540
+ async (audioBlob, duration) => {
3541
+ if (!config.onUploadFile) {
3542
+ setError("File upload is not configured");
3543
+ return;
3544
+ }
3545
+ try {
3546
+ const url = await config.onUploadFile(audioBlob, "voice-message.webm");
3547
+ await dispatchSend({
3548
+ body: "Voice message",
3549
+ type: "audio",
3550
+ mediaUrl: url,
3551
+ mediaDuration: duration
3552
+ });
3553
+ } catch (err) {
3554
+ setError(err instanceof Error ? err.message : "Failed to send voice message");
3555
+ config.onError?.({
3556
+ code: "SEND_ERROR",
3557
+ message: "Failed to send voice message",
3558
+ details: err
3559
+ });
3560
+ }
3561
+ },
3562
+ [config, dispatchSend]
3563
+ );
3564
+ return {
3565
+ hasConversation: !!preview,
3566
+ messages,
3567
+ participants,
3568
+ unreadCount,
3569
+ isLoading,
3570
+ isSubscribed,
3571
+ error,
3572
+ sendMessage,
3573
+ sendAudioMessage,
3574
+ containerRef
3575
+ };
3576
+ }
3577
+ function CollabInline({
3578
+ orderId,
3579
+ patientData,
3580
+ participantIds,
3581
+ onExpand,
3582
+ messageLimit = 5,
3583
+ placeholder = "Type a message about this case...",
3584
+ className,
3585
+ style
3586
+ }) {
3587
+ const {
3588
+ hasConversation,
3589
+ messages,
3590
+ unreadCount,
3591
+ isLoading,
3592
+ isSubscribed,
3593
+ error,
3594
+ sendMessage,
3595
+ sendAudioMessage,
3596
+ containerRef
3597
+ } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
3598
+ if (isLoading && !hasConversation) {
3599
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...styles13.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.loadingState, children: "Loading..." }) });
3600
+ }
3601
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className, style: { ...styles13.container, ...style }, children: [
3602
+ hasConversation && messages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsxRuntime.jsx(InlineMessageRow, { message }, message.id)) }),
3603
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.error, children: error }),
3604
+ /* @__PURE__ */ jsxRuntime.jsx(
3605
+ InlineInputBar,
3606
+ {
3607
+ onSend: sendMessage,
3608
+ onSendAudio: sendAudioMessage,
3609
+ placeholder,
3610
+ unreadCount: hasConversation ? unreadCount : 0,
3611
+ isLive: isSubscribed,
3612
+ showExpand: hasConversation && !!onExpand,
3613
+ onExpand
3614
+ }
3615
+ )
3616
+ ] });
3617
+ }
3618
+ function InlineMessageRow({ message }) {
3619
+ if (message.type === "system") {
3620
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.systemRow, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.systemText, children: message.body }) });
3621
+ }
3622
+ if (message.type === "audio" && message.mediaUrl) {
3623
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.messageRow, children: [
3624
+ /* @__PURE__ */ jsxRuntime.jsx(RoleDot, { role: message.senderRole }),
3625
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles13.senderName, children: [
3626
+ message.senderName,
3627
+ ":"
3628
+ ] }),
3629
+ /* @__PURE__ */ jsxRuntime.jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration })
3630
+ ] });
3631
+ }
3632
+ const preview = renderMessagePreview(message);
3633
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.messageRow, children: [
3634
+ /* @__PURE__ */ jsxRuntime.jsx(RoleDot, { role: message.senderRole }),
3635
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles13.senderName, children: [
3636
+ message.senderName,
3637
+ ":"
3638
+ ] }),
3639
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.messageText, children: preview })
3640
+ ] });
3641
+ }
3642
+ function RoleDot({ role }) {
3643
+ const colors = {
3644
+ radiologist: "#7c3aed",
3645
+ lab: "#2563eb",
3646
+ physician: "#059669",
3647
+ admin: "#dc2626"
3648
+ };
3649
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.roleDot, backgroundColor: colors[role] } });
3650
+ }
3651
+ function InlineAudioPlayer({ url, duration }) {
3652
+ const audioRef = react.useRef(null);
3653
+ const [isPlaying, setIsPlaying] = react.useState(false);
3654
+ const [currentTime, setCurrentTime] = react.useState(0);
3655
+ const toggle = react.useCallback(
3656
+ (e) => {
3657
+ e.stopPropagation();
3658
+ const audio = audioRef.current;
3659
+ if (!audio) return;
3660
+ if (isPlaying) {
3661
+ audio.pause();
3662
+ } else {
3663
+ audio.play().catch(() => {
3664
+ });
3665
+ }
3666
+ },
3667
+ [isPlaying]
3668
+ );
3669
+ react.useEffect(() => {
3670
+ const audio = audioRef.current;
3671
+ if (!audio) return;
3672
+ const onPlay = () => setIsPlaying(true);
3673
+ const onPause = () => setIsPlaying(false);
3674
+ const onEnded = () => {
3675
+ setIsPlaying(false);
3676
+ setCurrentTime(0);
3677
+ };
3678
+ const onTimeUpdate = () => setCurrentTime(audio.currentTime);
3679
+ audio.addEventListener("play", onPlay);
3680
+ audio.addEventListener("pause", onPause);
3681
+ audio.addEventListener("ended", onEnded);
3682
+ audio.addEventListener("timeupdate", onTimeUpdate);
3683
+ return () => {
3684
+ audio.removeEventListener("play", onPlay);
3685
+ audio.removeEventListener("pause", onPause);
3686
+ audio.removeEventListener("ended", onEnded);
3687
+ audio.removeEventListener("timeupdate", onTimeUpdate);
3688
+ };
3689
+ }, []);
3690
+ const total = duration ?? 0;
3691
+ const remaining = Math.max(0, total - Math.floor(currentTime));
3692
+ const displayTime = isPlaying ? remaining : total;
3693
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles13.audioPlayer, children: [
3694
+ /* @__PURE__ */ jsxRuntime.jsx(
3695
+ "button",
3696
+ {
3697
+ onClick: toggle,
3698
+ style: styles13.audioButton,
3699
+ title: isPlaying ? "Pause" : "Play",
3700
+ type: "button",
3701
+ children: isPlaying ? "\u23F8" : "\u25B6"
3702
+ }
3703
+ ),
3704
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles13.audioWaveform, children: [
3705
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "40%" } }),
3706
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "80%" } }),
3707
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "60%" } }),
3708
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "90%" } }),
3709
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "50%" } }),
3710
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "70%" } }),
3711
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles13.waveBar, height: "40%" } })
3712
+ ] }),
3713
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.audioDuration, children: formatDuration3(displayTime) }),
3714
+ /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
3715
+ ] });
3716
+ }
3717
+ function renderMessagePreview(message) {
3718
+ switch (message.type) {
3719
+ case "image":
3720
+ return `\u{1F4F7} ${message.fileName || "Image"}`;
3721
+ case "file":
3722
+ return `\u{1F4CE} ${message.fileName || "File"}`;
3723
+ case "deep_link":
3724
+ return `\u{1F517} ${message.body}`;
3725
+ default:
3726
+ return message.body;
3727
+ }
3728
+ }
3729
+ function formatDuration3(seconds) {
3730
+ const safe = Math.max(0, Math.floor(seconds));
3731
+ const m = Math.floor(safe / 60);
3732
+ const s = safe % 60;
3733
+ return `${m}:${s.toString().padStart(2, "0")}`;
3734
+ }
3735
+ function InlineInputBar({
3736
+ onSend,
3737
+ onSendAudio,
3738
+ placeholder,
3739
+ unreadCount,
3740
+ isLive,
3741
+ showExpand,
3742
+ onExpand
3743
+ }) {
3744
+ const [text, setText] = react.useState("");
3745
+ const [isSending, setIsSending] = react.useState(false);
3746
+ const inputRef = react.useRef(null);
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 () => {
3757
+ const trimmed = text.trim();
3758
+ if (!trimmed || isSending) return;
3759
+ setIsSending(true);
3760
+ try {
3761
+ await onSend(trimmed);
3762
+ setText("");
3763
+ inputRef.current?.focus();
3764
+ } finally {
3765
+ setIsSending(false);
3766
+ }
3767
+ }, [text, isSending, onSend]);
3768
+ const handleKeyDown = react.useCallback(
3769
+ (e) => {
3770
+ if (e.key === "Enter" && !e.shiftKey) {
3771
+ e.preventDefault();
3772
+ handleSend();
3773
+ }
3774
+ },
3775
+ [handleSend]
3776
+ );
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
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.inputBar, children: [
3818
+ /* @__PURE__ */ jsxRuntime.jsx(
3819
+ "input",
3820
+ {
3821
+ ref: inputRef,
3822
+ type: "text",
3823
+ value: text,
3824
+ onChange: (e) => setText(e.target.value),
3825
+ onKeyDown: handleKeyDown,
3826
+ placeholder: micError ?? placeholder,
3827
+ disabled: isSending,
3828
+ style: styles13.input
3829
+ }
3830
+ ),
3831
+ unreadCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
3832
+ 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
+ text.trim() && /* @__PURE__ */ jsxRuntime.jsx(
3845
+ "button",
3846
+ {
3847
+ onClick: handleSend,
3848
+ disabled: isSending,
3849
+ style: styles13.sendButton,
3850
+ title: "Send message",
3851
+ type: "button",
3852
+ children: "\u27A4"
3853
+ }
3854
+ ),
3855
+ showExpand && onExpand && /* @__PURE__ */ jsxRuntime.jsx(
3856
+ "button",
3857
+ {
3858
+ onClick: onExpand,
3859
+ style: styles13.expandButton,
3860
+ title: "Expand to full chat",
3861
+ type: "button",
3862
+ children: "\u26F6"
3863
+ }
3864
+ )
3865
+ ] });
3866
+ }
3867
+ var styles13 = {
3868
+ container: {
3869
+ display: "flex",
3870
+ flexDirection: "column",
3871
+ width: "100%",
3872
+ padding: "6px 8px",
3873
+ backgroundColor: "#ffffff",
3874
+ border: "1px solid #e5e7eb",
3875
+ borderRadius: "8px",
3876
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
3877
+ },
3878
+ loadingState: {
3879
+ padding: "8px",
3880
+ fontSize: "12px",
3881
+ color: "#9ca3af",
3882
+ textAlign: "center"
3883
+ },
3884
+ error: {
3885
+ padding: "4px 8px",
3886
+ marginBottom: "4px",
3887
+ fontSize: "11px",
3888
+ color: "#dc2626",
3889
+ backgroundColor: "#fef2f2",
3890
+ borderRadius: "4px"
3891
+ },
3892
+ preview: {
3893
+ display: "flex",
3894
+ flexDirection: "column",
3895
+ gap: "2px",
3896
+ marginBottom: "6px",
3897
+ paddingBottom: "6px",
3898
+ borderBottom: "1px solid #f3f4f6"
3899
+ },
3900
+ messageRow: {
3901
+ display: "flex",
3902
+ alignItems: "center",
3903
+ gap: "6px",
3904
+ fontSize: "12px",
3905
+ lineHeight: "1.4",
3906
+ overflow: "hidden"
3907
+ },
3908
+ roleDot: {
3909
+ flexShrink: 0,
3910
+ width: "6px",
3911
+ height: "6px",
3912
+ borderRadius: "50%"
3913
+ },
3914
+ senderName: {
3915
+ flexShrink: 0,
3916
+ fontWeight: 600,
3917
+ color: "#374151"
3918
+ },
3919
+ messageText: {
3920
+ color: "#4b5563",
3921
+ overflow: "hidden",
3922
+ textOverflow: "ellipsis",
3923
+ whiteSpace: "nowrap",
3924
+ minWidth: 0,
3925
+ flex: 1
3926
+ },
3927
+ systemRow: {
3928
+ fontSize: "11px",
3929
+ color: "#9ca3af",
3930
+ fontStyle: "italic",
3931
+ textAlign: "center",
3932
+ padding: "2px 0"
3933
+ },
3934
+ systemText: {
3935
+ opacity: 0.8
3936
+ },
3937
+ // ── Audio player (inline) ──
3938
+ audioPlayer: {
3939
+ display: "inline-flex",
3940
+ alignItems: "center",
3941
+ gap: "6px",
3942
+ padding: "2px 8px",
3943
+ borderRadius: "12px",
3944
+ backgroundColor: "#eff6ff",
3945
+ border: "1px solid #dbeafe",
3946
+ flex: 1,
3947
+ minWidth: 0
3948
+ },
3949
+ audioButton: {
3950
+ width: "20px",
3951
+ height: "20px",
3952
+ display: "flex",
3953
+ alignItems: "center",
3954
+ justifyContent: "center",
3955
+ fontSize: "10px",
3956
+ color: "#ffffff",
3957
+ backgroundColor: "#2563eb",
3958
+ border: "none",
3959
+ borderRadius: "50%",
3960
+ cursor: "pointer",
3961
+ flexShrink: 0
3962
+ },
3963
+ audioWaveform: {
3964
+ display: "inline-flex",
3965
+ alignItems: "center",
3966
+ gap: "2px",
3967
+ height: "16px",
3968
+ flex: 1,
3969
+ minWidth: 0
3970
+ },
3971
+ waveBar: {
3972
+ display: "inline-block",
3973
+ width: "2px",
3974
+ borderRadius: "1px",
3975
+ backgroundColor: "#60a5fa"
3976
+ },
3977
+ audioDuration: {
3978
+ fontSize: "11px",
3979
+ color: "#2563eb",
3980
+ fontVariantNumeric: "tabular-nums",
3981
+ fontWeight: 500,
3982
+ flexShrink: 0
3983
+ },
3984
+ // ── Input bar ──
3985
+ inputBar: {
3986
+ display: "flex",
3987
+ alignItems: "center",
3988
+ gap: "6px"
3989
+ },
3990
+ input: {
3991
+ flex: 1,
3992
+ padding: "6px 10px",
3993
+ fontSize: "13px",
3994
+ border: "1px solid #e5e7eb",
3995
+ borderRadius: "16px",
3996
+ outline: "none",
3997
+ fontFamily: "inherit",
3998
+ minWidth: 0
3999
+ },
4000
+ unreadBadge: {
4001
+ flexShrink: 0,
4002
+ minWidth: "18px",
4003
+ height: "18px",
4004
+ padding: "0 5px",
4005
+ fontSize: "10px",
4006
+ fontWeight: 600,
4007
+ color: "#ffffff",
4008
+ backgroundColor: "#dc2626",
4009
+ borderRadius: "10px",
4010
+ display: "flex",
4011
+ alignItems: "center",
4012
+ justifyContent: "center"
4013
+ },
4014
+ liveDot: {
4015
+ flexShrink: 0,
4016
+ width: "6px",
4017
+ height: "6px",
4018
+ borderRadius: "50%",
4019
+ backgroundColor: "#22c55e"
4020
+ },
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
+ sendButton: {
4035
+ flexShrink: 0,
4036
+ width: "28px",
4037
+ height: "28px",
4038
+ display: "flex",
4039
+ alignItems: "center",
4040
+ justifyContent: "center",
4041
+ fontSize: "13px",
4042
+ color: "#ffffff",
4043
+ backgroundColor: "#2563eb",
4044
+ border: "none",
4045
+ borderRadius: "50%",
4046
+ cursor: "pointer"
4047
+ },
4048
+ expandButton: {
4049
+ flexShrink: 0,
4050
+ width: "28px",
4051
+ height: "28px",
4052
+ display: "flex",
4053
+ alignItems: "center",
4054
+ justifyContent: "center",
4055
+ fontSize: "12px",
4056
+ color: "#6b7280",
4057
+ backgroundColor: "transparent",
4058
+ border: "1px solid #e5e7eb",
4059
+ borderRadius: "50%",
4060
+ 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
+ };
4109
+ function useConversationList(options) {
4110
+ const enabled = options?.enabled ?? true;
4111
+ const { fetchConversationList, config, totalUnread: serverTotalUnread } = useCollab();
4112
+ const [conversations, setConversations] = react.useState([]);
4113
+ const [isLoading, setIsLoading] = react.useState(enabled);
4114
+ const [error, setError] = react.useState(null);
4115
+ const silentRefresh = react.useCallback(async () => {
4116
+ try {
4117
+ const list = await fetchConversationList();
4118
+ setConversations(list);
4119
+ } catch (err) {
4120
+ config.onError?.({
4121
+ code: "LIST_ERROR",
4122
+ message: err instanceof Error ? err.message : "Failed to load conversations",
4123
+ details: err
4124
+ });
4125
+ }
4126
+ }, [fetchConversationList, config]);
4127
+ const refresh = react.useCallback(async () => {
4128
+ setIsLoading(true);
4129
+ setError(null);
4130
+ try {
4131
+ const list = await fetchConversationList();
4132
+ setConversations(list);
4133
+ } catch (err) {
4134
+ const msg = err instanceof Error ? err.message : "Failed to load conversations";
4135
+ setError(msg);
4136
+ config.onError?.({
4137
+ code: "LIST_ERROR",
4138
+ message: msg,
4139
+ details: err
4140
+ });
4141
+ } finally {
4142
+ setIsLoading(false);
4143
+ }
4144
+ }, [fetchConversationList, config]);
4145
+ react.useEffect(() => {
4146
+ if (!enabled) return;
4147
+ refresh();
4148
+ }, [enabled, refresh]);
4149
+ const hasHandledInitialUnreadRef = react.useRef(false);
4150
+ react.useEffect(() => {
4151
+ if (!enabled) return;
4152
+ if (!hasHandledInitialUnreadRef.current) {
4153
+ hasHandledInitialUnreadRef.current = true;
4154
+ return;
4155
+ }
4156
+ silentRefresh();
4157
+ }, [serverTotalUnread, enabled]);
4158
+ const filter = react.useCallback(
4159
+ (query) => {
4160
+ const q = query.trim().toLowerCase();
4161
+ if (!q) return conversations;
4162
+ return conversations.filter((c) => {
4163
+ const patient = c.patientSnapshot?.patientName?.toLowerCase() || "";
4164
+ const name = c.name.toLowerCase();
4165
+ return name.includes(q) || patient.includes(q);
4166
+ });
4167
+ },
4168
+ [conversations]
4169
+ );
4170
+ const totalUnread = conversations.reduce((sum, c) => sum + c.unreadCount, 0);
4171
+ return {
4172
+ conversations,
4173
+ isLoading,
4174
+ error,
4175
+ refresh,
4176
+ filter,
4177
+ totalUnread
4178
+ };
4179
+ }
4180
+ function ConversationListItem({
4181
+ item,
4182
+ isSelected,
4183
+ onClick,
4184
+ className
4185
+ }) {
4186
+ const hasUnread = item.unreadCount > 0;
4187
+ const displayName = item.name || item.patientSnapshot?.patientName || "Unknown";
4188
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4189
+ "button",
4190
+ {
4191
+ className,
4192
+ onClick,
4193
+ style: {
4194
+ ...styles14.container,
4195
+ ...isSelected ? styles14.selected : {},
4196
+ ...hasUnread ? styles14.unread : {}
4197
+ },
4198
+ type: "button",
4199
+ children: [
4200
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles14.avatarWrapper, children: [
4201
+ item.picture ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: item.picture, alt: "", style: styles14.avatar }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.avatarFallback, children: (displayName).charAt(0).toUpperCase() }),
4202
+ hasUnread && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles14.unreadDot })
4203
+ ] }),
4204
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles14.content, children: [
4205
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles14.topRow, children: [
4206
+ /* @__PURE__ */ jsxRuntime.jsx(
4207
+ "span",
4208
+ {
4209
+ style: {
4210
+ ...styles14.name,
4211
+ ...hasUnread ? styles14.nameUnread : {}
4212
+ },
4213
+ children: displayName
4214
+ }
4215
+ ),
4216
+ /* @__PURE__ */ jsxRuntime.jsx(
4217
+ "span",
4218
+ {
4219
+ style: {
4220
+ ...styles14.time,
4221
+ ...hasUnread ? styles14.timeUnread : {}
4222
+ },
4223
+ children: formatRelativeTime(item.lastActivityAt)
4224
+ }
4225
+ )
4226
+ ] }),
4227
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles14.bottomRow, children: [
4228
+ /* @__PURE__ */ jsxRuntime.jsx(
4229
+ "span",
4230
+ {
4231
+ style: {
4232
+ ...styles14.preview,
4233
+ ...hasUnread ? styles14.previewUnread : {}
4234
+ },
4235
+ children: renderLastMessagePreview(item.lastMessage)
4236
+ }
4237
+ ),
4238
+ hasUnread && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles14.badge, children: item.unreadCount > 99 ? "99+" : item.unreadCount })
4239
+ ] })
4240
+ ] })
4241
+ ]
4242
+ }
4243
+ );
4244
+ }
4245
+ function renderLastMessagePreview(message) {
4246
+ if (!message) return "No messages yet";
4247
+ const prefix = message.senderName ? `${message.senderName}: ` : "";
4248
+ switch (message.type) {
4249
+ case "audio":
4250
+ return `${prefix}\u{1F3A4} Voice message`;
4251
+ case "image":
4252
+ return `${prefix}\u{1F4F7} ${message.fileName || "Image"}`;
4253
+ case "file":
4254
+ return `${prefix}\u{1F4CE} ${message.fileName || "File"}`;
4255
+ case "deep_link":
4256
+ return `${prefix}\u{1F517} ${message.body}`;
4257
+ case "system":
4258
+ return message.body;
4259
+ default: {
4260
+ const body = message.body || "";
4261
+ const preview = body.length > 60 ? `${body.slice(0, 60)}\u2026` : body;
4262
+ return `${prefix}${preview}`;
4263
+ }
4264
+ }
4265
+ }
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
+ var styles14 = {
4284
+ container: {
4285
+ display: "flex",
4286
+ alignItems: "center",
4287
+ gap: "12px",
4288
+ padding: "10px 12px",
4289
+ width: "100%",
4290
+ backgroundColor: "transparent",
4291
+ border: "none",
4292
+ borderBottom: "1px solid #f3f4f6",
4293
+ cursor: "pointer",
4294
+ textAlign: "left"
4295
+ },
4296
+ selected: {
4297
+ backgroundColor: "#eff6ff"
4298
+ },
4299
+ unread: {},
4300
+ avatarWrapper: {
4301
+ position: "relative",
4302
+ width: "40px",
4303
+ height: "40px",
4304
+ flexShrink: 0
4305
+ },
4306
+ avatar: {
4307
+ width: "40px",
4308
+ height: "40px",
4309
+ borderRadius: "50%",
4310
+ objectFit: "cover"
4311
+ },
4312
+ avatarFallback: {
4313
+ width: "40px",
4314
+ height: "40px",
4315
+ borderRadius: "50%",
4316
+ backgroundColor: "#e5e7eb",
4317
+ display: "flex",
4318
+ alignItems: "center",
4319
+ justifyContent: "center",
4320
+ fontSize: "15px",
4321
+ fontWeight: 600,
4322
+ color: "#6b7280"
4323
+ },
4324
+ unreadDot: {
4325
+ position: "absolute",
4326
+ top: "0",
4327
+ left: "0",
4328
+ width: "10px",
4329
+ height: "10px",
4330
+ borderRadius: "50%",
4331
+ backgroundColor: "#2563eb",
4332
+ border: "2px solid #ffffff"
4333
+ },
4334
+ content: {
4335
+ flex: 1,
4336
+ display: "flex",
4337
+ flexDirection: "column",
4338
+ gap: "2px",
4339
+ minWidth: 0
4340
+ },
4341
+ topRow: {
4342
+ display: "flex",
4343
+ justifyContent: "space-between",
4344
+ alignItems: "baseline",
4345
+ gap: "8px"
4346
+ },
4347
+ name: {
4348
+ fontSize: "14px",
4349
+ fontWeight: 500,
4350
+ color: "#111827",
4351
+ overflow: "hidden",
4352
+ textOverflow: "ellipsis",
4353
+ whiteSpace: "nowrap",
4354
+ flex: 1,
4355
+ minWidth: 0
4356
+ },
4357
+ nameUnread: {
4358
+ fontWeight: 700
4359
+ },
4360
+ time: {
4361
+ fontSize: "11px",
4362
+ color: "#9ca3af",
4363
+ flexShrink: 0
4364
+ },
4365
+ timeUnread: {
4366
+ color: "#2563eb",
4367
+ fontWeight: 600
4368
+ },
4369
+ bottomRow: {
4370
+ display: "flex",
4371
+ justifyContent: "space-between",
4372
+ alignItems: "center",
4373
+ gap: "8px"
4374
+ },
4375
+ preview: {
4376
+ fontSize: "13px",
4377
+ color: "#6b7280",
4378
+ overflow: "hidden",
4379
+ textOverflow: "ellipsis",
4380
+ whiteSpace: "nowrap",
4381
+ flex: 1,
4382
+ minWidth: 0
4383
+ },
4384
+ previewUnread: {
4385
+ color: "#111827",
4386
+ fontWeight: 500
4387
+ },
4388
+ badge: {
4389
+ minWidth: "20px",
4390
+ height: "20px",
4391
+ padding: "0 6px",
4392
+ fontSize: "11px",
4393
+ fontWeight: 600,
4394
+ color: "#ffffff",
4395
+ backgroundColor: "#2563eb",
4396
+ borderRadius: "10px",
4397
+ display: "flex",
4398
+ alignItems: "center",
4399
+ justifyContent: "center",
4400
+ flexShrink: 0
4401
+ }
4402
+ };
4403
+ function ConversationList({
4404
+ conversations,
4405
+ selectedId,
4406
+ isLoading,
4407
+ error,
4408
+ onSelect,
4409
+ title = "Conversations",
4410
+ className
4411
+ }) {
4412
+ const [query, setQuery] = react.useState("");
4413
+ const [showUnreadOnly, setShowUnreadOnly] = react.useState(false);
4414
+ const filtered = react.useMemo(() => {
4415
+ let result = conversations;
4416
+ if (showUnreadOnly) {
4417
+ result = result.filter((c) => c.unreadCount > 0);
4418
+ }
4419
+ const q = query.trim().toLowerCase();
4420
+ if (q) {
4421
+ result = result.filter((c) => {
4422
+ const patient = c.patientSnapshot?.patientName?.toLowerCase() || "";
4423
+ const name = c.name.toLowerCase();
4424
+ return name.includes(q) || patient.includes(q);
4425
+ });
4426
+ }
4427
+ return result;
4428
+ }, [conversations, query, showUnreadOnly]);
4429
+ const totalUnread = conversations.reduce((sum, c) => sum + c.unreadCount, 0);
4430
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles15.container, children: [
4431
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles15.header, children: [
4432
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles15.titleRow, children: [
4433
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: styles15.title, children: title }),
4434
+ totalUnread > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles15.totalBadge, children: totalUnread })
4435
+ ] }),
4436
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles15.searchRow, children: /* @__PURE__ */ jsxRuntime.jsx(
4437
+ "input",
4438
+ {
4439
+ type: "text",
4440
+ value: query,
4441
+ onChange: (e) => setQuery(e.target.value),
4442
+ placeholder: "Search patients or channels...",
4443
+ style: styles15.searchInput
4444
+ }
4445
+ ) }),
4446
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles15.filterRow, children: [
4447
+ /* @__PURE__ */ jsxRuntime.jsx(
4448
+ "button",
4449
+ {
4450
+ onClick: () => setShowUnreadOnly(false),
4451
+ style: {
4452
+ ...styles15.filterButton,
4453
+ ...!showUnreadOnly ? styles15.filterButtonActive : {}
4454
+ },
4455
+ type: "button",
4456
+ children: "All"
4457
+ }
4458
+ ),
4459
+ /* @__PURE__ */ jsxRuntime.jsxs(
4460
+ "button",
4461
+ {
4462
+ onClick: () => setShowUnreadOnly(true),
4463
+ style: {
4464
+ ...styles15.filterButton,
4465
+ ...showUnreadOnly ? styles15.filterButtonActive : {}
4466
+ },
4467
+ type: "button",
4468
+ children: [
4469
+ "Unread ",
4470
+ totalUnread > 0 && `(${totalUnread})`
4471
+ ]
4472
+ }
4473
+ )
4474
+ ] })
4475
+ ] }),
4476
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles15.list, children: [
4477
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles15.state, children: "Loading conversations..." }),
4478
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles15.errorState, children: error }),
4479
+ !isLoading && !error && filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles15.state, children: query ? "No matching conversations" : "No conversations yet" }),
4480
+ filtered.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
4481
+ ConversationListItem,
4482
+ {
4483
+ item,
4484
+ isSelected: selectedId === item.id,
4485
+ onClick: () => onSelect(item)
4486
+ },
4487
+ item.id
4488
+ ))
4489
+ ] })
4490
+ ] });
4491
+ }
4492
+ var styles15 = {
4493
+ container: {
4494
+ display: "flex",
4495
+ flexDirection: "column",
4496
+ height: "100%",
4497
+ backgroundColor: "#ffffff",
4498
+ borderRight: "1px solid #e5e7eb"
4499
+ },
4500
+ header: {
4501
+ padding: "12px",
4502
+ borderBottom: "1px solid #e5e7eb",
4503
+ backgroundColor: "#f9fafb",
4504
+ flexShrink: 0
4505
+ },
4506
+ titleRow: {
4507
+ display: "flex",
4508
+ alignItems: "center",
4509
+ gap: "8px",
4510
+ marginBottom: "8px"
4511
+ },
4512
+ title: {
4513
+ margin: 0,
4514
+ fontSize: "15px",
4515
+ fontWeight: 600,
4516
+ color: "#111827"
4517
+ },
4518
+ totalBadge: {
4519
+ minWidth: "20px",
4520
+ height: "20px",
4521
+ padding: "0 6px",
4522
+ fontSize: "11px",
4523
+ fontWeight: 600,
4524
+ color: "#ffffff",
4525
+ backgroundColor: "#2563eb",
4526
+ borderRadius: "10px",
4527
+ display: "flex",
4528
+ alignItems: "center",
4529
+ justifyContent: "center"
4530
+ },
4531
+ searchRow: {
4532
+ marginBottom: "8px"
4533
+ },
4534
+ searchInput: {
4535
+ width: "100%",
4536
+ padding: "6px 10px",
4537
+ fontSize: "13px",
4538
+ border: "1px solid #e5e7eb",
4539
+ borderRadius: "6px",
4540
+ outline: "none",
4541
+ fontFamily: "inherit"
4542
+ },
4543
+ filterRow: {
4544
+ display: "flex",
4545
+ gap: "4px"
4546
+ },
4547
+ filterButton: {
4548
+ flex: 1,
4549
+ padding: "5px 8px",
4550
+ fontSize: "12px",
4551
+ color: "#6b7280",
4552
+ backgroundColor: "#ffffff",
4553
+ border: "1px solid #e5e7eb",
4554
+ borderRadius: "6px",
4555
+ cursor: "pointer"
4556
+ },
4557
+ filterButtonActive: {
4558
+ color: "#ffffff",
4559
+ backgroundColor: "#2563eb",
4560
+ borderColor: "#2563eb",
4561
+ fontWeight: 500
4562
+ },
4563
+ list: {
4564
+ flex: 1,
4565
+ overflowY: "auto"
4566
+ },
4567
+ state: {
4568
+ padding: "24px 16px",
4569
+ fontSize: "13px",
4570
+ color: "#9ca3af",
4571
+ textAlign: "center"
4572
+ },
4573
+ errorState: {
4574
+ padding: "16px",
4575
+ fontSize: "13px",
4576
+ color: "#dc2626",
4577
+ backgroundColor: "#fef2f2",
4578
+ margin: "12px",
4579
+ borderRadius: "6px",
4580
+ textAlign: "center"
4581
+ }
4582
+ };
4583
+ function CollabInbox({
4584
+ initialConversationId,
4585
+ onSelectConversation,
4586
+ title,
4587
+ className,
4588
+ style
4589
+ }) {
4590
+ const { conversations, isLoading, error } = useConversationList();
4591
+ const [selectedId, setSelectedId] = react.useState(initialConversationId ?? null);
4592
+ const selected = conversations.find((c) => c.id === selectedId) || null;
4593
+ const handleSelect = (item) => {
4594
+ setSelectedId(item.id);
4595
+ onSelectConversation?.(item);
4596
+ };
4597
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...styles16.container, ...style }, children: [
4598
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles16.sidebar, children: /* @__PURE__ */ jsxRuntime.jsx(
4599
+ ConversationList,
4600
+ {
4601
+ conversations,
4602
+ selectedId,
4603
+ isLoading,
4604
+ error,
4605
+ onSelect: handleSelect,
4606
+ title
4607
+ }
4608
+ ) }),
4609
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles16.main, children: selected ? /* @__PURE__ */ jsxRuntime.jsx(
4610
+ CollabPanel,
4611
+ {
4612
+ orderId: selected.orderId,
4613
+ patientData: buildPatientData(selected),
4614
+ showSeenBy: true,
4615
+ style: styles16.panel
4616
+ },
4617
+ selected.id
4618
+ ) : /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { hasAny: conversations.length > 0 }) })
4619
+ ] });
4620
+ }
4621
+ function buildPatientData(item) {
4622
+ if (item.patientSnapshot) return item.patientSnapshot;
4623
+ return {
4624
+ patientName: item.name,
4625
+ orderId: item.orderId
4626
+ };
4627
+ }
4628
+ function EmptyState({ hasAny }) {
4629
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles16.empty, children: [
4630
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles16.emptyIcon, children: "\u{1F4AC}" }),
4631
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: styles16.emptyTitle, children: hasAny ? "Select a conversation" : "No conversations yet" }),
4632
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles16.emptySubtitle, children: hasAny ? "Pick a patient from the list to start collaborating" : "Conversations appear here once you start messaging about a case" })
4633
+ ] });
4634
+ }
4635
+ var styles16 = {
4636
+ container: {
4637
+ display: "flex",
4638
+ height: "100%",
4639
+ width: "100%",
4640
+ backgroundColor: "#ffffff",
4641
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
4642
+ },
4643
+ sidebar: {
4644
+ width: "320px",
4645
+ minWidth: "280px",
4646
+ flexShrink: 0,
4647
+ display: "flex",
4648
+ flexDirection: "column"
4649
+ },
4650
+ main: {
4651
+ flex: 1,
4652
+ display: "flex",
4653
+ flexDirection: "column",
4654
+ minWidth: 0,
4655
+ padding: "12px",
4656
+ backgroundColor: "#f3f4f6"
4657
+ },
4658
+ panel: {
4659
+ height: "100%",
4660
+ width: "100%"
4661
+ },
4662
+ empty: {
4663
+ flex: 1,
4664
+ display: "flex",
4665
+ flexDirection: "column",
4666
+ alignItems: "center",
4667
+ justifyContent: "center",
4668
+ padding: "48px 24px",
4669
+ textAlign: "center"
4670
+ },
4671
+ emptyIcon: {
4672
+ fontSize: "48px",
4673
+ marginBottom: "12px"
4674
+ },
4675
+ emptyTitle: {
4676
+ margin: "0 0 6px",
4677
+ fontSize: "16px",
4678
+ fontWeight: 600,
4679
+ color: "#374151"
4680
+ },
4681
+ emptySubtitle: {
4682
+ margin: 0,
4683
+ fontSize: "13px",
4684
+ color: "#6b7280",
4685
+ maxWidth: "320px"
4686
+ }
4687
+ };
4688
+ function useMessages({
4689
+ conversationId,
4690
+ initialLimit = MESSAGES_PAGE_SIZE
4691
+ }) {
4692
+ const { fetchMessages, config } = useCollab();
4693
+ const [messages, setMessages] = react.useState([]);
4694
+ const [isLoading, setIsLoading] = react.useState(false);
4695
+ const [hasMore, setHasMore] = react.useState(true);
4696
+ const [error, setError] = react.useState(null);
4697
+ const loadedRef = react.useRef(null);
4698
+ react.useEffect(() => {
4699
+ if (!conversationId) {
4700
+ setMessages([]);
4701
+ setHasMore(false);
4702
+ loadedRef.current = null;
4703
+ return;
4704
+ }
4705
+ if (loadedRef.current === conversationId) return;
4706
+ let cancelled = false;
4707
+ const loadInitial = async () => {
4708
+ setIsLoading(true);
4709
+ setError(null);
4710
+ try {
4711
+ const initial = await fetchMessages(conversationId, { limit: initialLimit });
4712
+ if (cancelled) return;
4713
+ setMessages(initial);
4714
+ setHasMore(initial.length >= initialLimit);
4715
+ loadedRef.current = conversationId;
4716
+ } catch (err) {
4717
+ if (cancelled) return;
4718
+ const msg = err instanceof Error ? err.message : "Failed to load messages";
4719
+ setError(msg);
4720
+ config.onError?.({
4721
+ code: "LOAD_ERROR",
4722
+ message: "Failed to load messages",
4723
+ details: err
4724
+ });
4725
+ } finally {
4726
+ if (!cancelled) setIsLoading(false);
4727
+ }
4728
+ };
4729
+ loadInitial();
4730
+ return () => {
4731
+ cancelled = true;
4732
+ };
4733
+ }, [conversationId, initialLimit, fetchMessages, config]);
4734
+ const loadMore = react.useCallback(async () => {
4735
+ if (!conversationId || !hasMore || isLoading) return;
4736
+ const oldestId = messages[0]?.id;
4737
+ setIsLoading(true);
4738
+ try {
4739
+ const older = await fetchMessages(conversationId, {
4740
+ limit: initialLimit,
4741
+ before: oldestId
4742
+ });
4743
+ setMessages((prev) => [...older, ...prev]);
4744
+ setHasMore(older.length >= initialLimit);
4745
+ } catch (err) {
4746
+ config.onError?.({
4747
+ code: "LOAD_ERROR",
4748
+ message: "Failed to load older messages",
4749
+ details: err
4750
+ });
4751
+ } finally {
4752
+ setIsLoading(false);
4753
+ }
4754
+ }, [conversationId, hasMore, isLoading, messages, initialLimit, fetchMessages, config]);
4755
+ const addMessage = react.useCallback((message) => {
4756
+ setMessages((prev) => [...prev, message]);
4757
+ }, []);
4758
+ const updateMessageReadBy = react.useCallback((messageId, userId) => {
4759
+ setMessages(
4760
+ (prev) => prev.map(
4761
+ (msg) => msg.id === messageId && !(msg.readBy ?? []).includes(userId) ? { ...msg, readBy: [...msg.readBy ?? [], userId] } : msg
4762
+ )
4763
+ );
4764
+ }, []);
4765
+ const reset = react.useCallback(() => {
4766
+ setMessages([]);
4767
+ setHasMore(true);
4768
+ setError(null);
4769
+ loadedRef.current = null;
4770
+ }, []);
4771
+ return {
4772
+ messages,
4773
+ isLoading,
4774
+ hasMore,
4775
+ error,
4776
+ loadMore,
4777
+ addMessage,
4778
+ updateMessageReadBy,
4779
+ reset
4780
+ };
4781
+ }
4782
+ function useUnreadCount() {
4783
+ const { socket, totalUnread } = useCollab();
4784
+ const [counts, setCounts] = react.useState({});
4785
+ react.useEffect(() => {
4786
+ socket.onUnreadCountUpdate((serverCounts) => {
4787
+ setCounts(serverCounts);
4788
+ });
4789
+ }, [socket]);
4790
+ const getCountForConversation = react.useCallback(
4791
+ (conversationId) => {
4792
+ return counts[conversationId] ?? 0;
4793
+ },
4794
+ [counts]
4795
+ );
4796
+ const clearForConversation = react.useCallback((conversationId) => {
4797
+ setCounts((prev) => {
4798
+ const next = { ...prev };
4799
+ delete next[conversationId];
4800
+ return next;
4801
+ });
4802
+ }, []);
4803
+ return {
4804
+ totalUnread,
4805
+ getCountForConversation,
4806
+ clearForConversation
4807
+ };
4808
+ }
4809
+ function usePinnedMessages({
4810
+ conversationId,
4811
+ initial = []
4812
+ }) {
4813
+ const { pinMessage, unpinMessage, fetchPinnedMessages, config } = useCollab();
4814
+ const [pinnedMessages, setPinnedMessages] = react.useState(initial);
4815
+ const [isLoading, setIsLoading] = react.useState(false);
4816
+ const [error, setError] = react.useState(null);
4817
+ const refresh = react.useCallback(async () => {
4818
+ if (!conversationId) return;
4819
+ setIsLoading(true);
4820
+ setError(null);
4821
+ try {
4822
+ const pinned = await fetchPinnedMessages(conversationId);
4823
+ setPinnedMessages(pinned);
4824
+ } catch (err) {
4825
+ const msg = err instanceof Error ? err.message : "Failed to load pinned messages";
4826
+ setError(msg);
4827
+ } finally {
4828
+ setIsLoading(false);
4829
+ }
4830
+ }, [conversationId, fetchPinnedMessages]);
4831
+ react.useEffect(() => {
4832
+ if (conversationId && initial.length === 0) {
4833
+ refresh();
4834
+ }
4835
+ }, [conversationId]);
4836
+ const pin = react.useCallback(
4837
+ async (messageId) => {
4838
+ if (!conversationId) return;
4839
+ setError(null);
4840
+ try {
4841
+ await pinMessage(conversationId, messageId);
4842
+ } catch (err) {
4843
+ const msg = err instanceof Error ? err.message : "Failed to pin message";
4844
+ setError(msg);
4845
+ config.onError?.({
4846
+ code: "PIN_ERROR",
4847
+ message: msg,
4848
+ details: err
4849
+ });
4850
+ }
4851
+ },
4852
+ [conversationId, pinMessage, config]
4853
+ );
4854
+ const unpin = react.useCallback(
4855
+ async (messageId) => {
4856
+ if (!conversationId) return;
4857
+ setError(null);
4858
+ try {
4859
+ await unpinMessage(conversationId, messageId);
4860
+ } catch (err) {
4861
+ const msg = err instanceof Error ? err.message : "Failed to unpin message";
4862
+ setError(msg);
4863
+ config.onError?.({
4864
+ code: "UNPIN_ERROR",
4865
+ message: msg,
4866
+ details: err
4867
+ });
4868
+ }
4869
+ },
4870
+ [conversationId, unpinMessage, config]
4871
+ );
4872
+ const handlePinned = react.useCallback((message) => {
4873
+ setPinnedMessages((prev) => {
4874
+ if (prev.some((m) => m.id === message.id)) return prev;
4875
+ return [...prev, message];
4876
+ });
4877
+ }, []);
4878
+ const handleUnpinned = react.useCallback((messageId) => {
4879
+ setPinnedMessages((prev) => prev.filter((m) => m.id !== messageId));
4880
+ }, []);
4881
+ return {
4882
+ pinnedMessages,
4883
+ isLoading,
4884
+ error,
4885
+ pin,
4886
+ unpin,
4887
+ handlePinned,
4888
+ handleUnpinned,
4889
+ refresh
4890
+ };
4891
+ }
4892
+
4893
+ exports.AUDIO_MIME_TYPE = AUDIO_MIME_TYPE;
4894
+ exports.ChannelSettings = ChannelSettings;
4895
+ exports.CollabInbox = CollabInbox;
4896
+ exports.CollabInline = CollabInline;
4897
+ exports.CollabPanel = CollabPanel;
4898
+ exports.CollabPopup = CollabPopup;
4899
+ exports.CollabProvider = CollabProvider;
4900
+ exports.CollabSocket = CollabSocket;
4901
+ exports.ConversationList = ConversationList;
4902
+ exports.ConversationListItem = ConversationListItem;
4903
+ exports.DEEP_LINK_PREFIX = DEEP_LINK_PREFIX;
4904
+ exports.EVENTS = EVENTS;
4905
+ exports.MAX_FILE_SIZE = MAX_FILE_SIZE;
4906
+ exports.MAX_PINNED_MESSAGES = MAX_PINNED_MESSAGES;
4907
+ exports.MESSAGES_PAGE_SIZE = MESSAGES_PAGE_SIZE;
4908
+ exports.MESSAGE_TYPES = MESSAGE_TYPES;
4909
+ exports.MessageActionsMenu = MessageActionsMenu;
4910
+ exports.MessageBubble = MessageBubble;
4911
+ exports.MessageInput = MessageInput;
4912
+ exports.MessageList = MessageList;
4913
+ exports.ParticipantsList = ParticipantsList;
4914
+ exports.PatientHeader = PatientHeader;
4915
+ exports.PinnedMessagesBar = PinnedMessagesBar;
4916
+ exports.ReplyPreview = ReplyPreview;
4917
+ exports.ReplyQuoteBlock = ReplyQuoteBlock;
4918
+ exports.SUPPORTED_IMAGE_TYPES = SUPPORTED_IMAGE_TYPES;
4919
+ exports.SeenByIndicator = SeenByIndicator;
4920
+ exports.TYPING_DEBOUNCE_MS = TYPING_DEBOUNCE_MS;
4921
+ exports.useAudioRecorder = useAudioRecorder;
4922
+ exports.useChannelSettings = useChannelSettings;
4923
+ exports.useCollab = useCollab;
4924
+ exports.useConversation = useConversation;
4925
+ exports.useConversationList = useConversationList;
4926
+ exports.useDeepLinks = useDeepLinks;
4927
+ exports.useInlineCollab = useInlineCollab;
4928
+ exports.useMessages = useMessages;
4929
+ exports.usePinnedMessages = usePinnedMessages;
4930
+ exports.useUnreadCount = useUnreadCount;
4931
+ //# sourceMappingURL=index.js.map
4932
+ //# sourceMappingURL=index.js.map