@natoe/colab 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +95 -12
- package/dist/index.d.ts +95 -12
- package/dist/index.js +295 -106
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +295 -106
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -60,6 +60,13 @@ function toCamelKey(key) {
|
|
|
60
60
|
var CollabSocket = class {
|
|
61
61
|
constructor() {
|
|
62
62
|
this.socket = null;
|
|
63
|
+
/**
|
|
64
|
+
* Conversation channels keyed by id. Each entry is reference-counted so
|
|
65
|
+
* multiple surfaces (e.g. inline chat + expanded panel mounted at once
|
|
66
|
+
* for the same conversation) can coexist without one's `leaveConversation`
|
|
67
|
+
* tearing the channel out from under the other. See `joinConversation`
|
|
68
|
+
* and the returned `ChannelSubscription.release`.
|
|
69
|
+
*/
|
|
63
70
|
this.channels = /* @__PURE__ */ new Map();
|
|
64
71
|
this.presences = /* @__PURE__ */ new Map();
|
|
65
72
|
this.userChannel = null;
|
|
@@ -130,59 +137,85 @@ var CollabSocket = class {
|
|
|
130
137
|
onUnreadCountUpdate(callback) {
|
|
131
138
|
this.onUnreadUpdate = callback;
|
|
132
139
|
}
|
|
133
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Join a conversation channel and subscribe to events.
|
|
142
|
+
*
|
|
143
|
+
* Reference-counted: multiple callers can join the same conversation
|
|
144
|
+
* (e.g. inline preview + expanded panel mounted side-by-side). Each
|
|
145
|
+
* call binds its own listeners and gets back a `ChannelSubscription`.
|
|
146
|
+
* The underlying channel only `.leave()`s the server when the LAST
|
|
147
|
+
* subscriber calls `release()`.
|
|
148
|
+
*/
|
|
134
149
|
joinConversation(conversationId, callbacks) {
|
|
135
150
|
if (!this.socket) return null;
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
let entry = this.channels.get(conversationId);
|
|
152
|
+
if (!entry) {
|
|
153
|
+
const channel2 = this.socket.channel(`conversation:${conversationId}`, {});
|
|
154
|
+
channel2.join().receive("ok", () => {
|
|
155
|
+
}).receive("error", (reason) => {
|
|
156
|
+
this.config?.onError?.({
|
|
157
|
+
code: "CHANNEL_JOIN_ERROR",
|
|
158
|
+
message: `Failed to join conversation ${conversationId}`,
|
|
159
|
+
details: reason
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
entry = { channel: channel2, subscribers: 0, presence: null };
|
|
163
|
+
this.channels.set(conversationId, entry);
|
|
138
164
|
}
|
|
139
|
-
const channel =
|
|
165
|
+
const channel = entry.channel;
|
|
166
|
+
const refs = [];
|
|
167
|
+
const bind = (event, fn) => {
|
|
168
|
+
const ref = channel.on(event, fn);
|
|
169
|
+
refs.push({ event, ref });
|
|
170
|
+
};
|
|
140
171
|
if (callbacks.onMessage) {
|
|
141
|
-
|
|
172
|
+
bind(EVENTS.MESSAGE_NEW, (payload) => {
|
|
142
173
|
callbacks.onMessage(snakeToCamel(payload));
|
|
143
174
|
});
|
|
144
175
|
}
|
|
145
176
|
if (callbacks.onTyping) {
|
|
146
|
-
|
|
177
|
+
bind(EVENTS.USER_TYPING, (payload) => {
|
|
147
178
|
callbacks.onTyping(snakeToCamel(payload));
|
|
148
179
|
});
|
|
149
180
|
}
|
|
150
181
|
if (callbacks.onUserJoined) {
|
|
151
|
-
|
|
182
|
+
bind(EVENTS.USER_JOINED, (payload) => {
|
|
152
183
|
callbacks.onUserJoined(snakeToCamel(payload));
|
|
153
184
|
});
|
|
154
185
|
}
|
|
155
186
|
if (callbacks.onUserLeft) {
|
|
156
|
-
|
|
187
|
+
bind(EVENTS.USER_LEFT, (payload) => {
|
|
157
188
|
callbacks.onUserLeft(snakeToCamel(payload));
|
|
158
189
|
});
|
|
159
190
|
}
|
|
160
191
|
if (callbacks.onChannelUpdated) {
|
|
161
|
-
|
|
192
|
+
bind(EVENTS.CHANNEL_UPDATED, (payload) => {
|
|
162
193
|
callbacks.onChannelUpdated(snakeToCamel(payload));
|
|
163
194
|
});
|
|
164
195
|
}
|
|
165
196
|
if (callbacks.onChannelDeleted) {
|
|
166
|
-
|
|
197
|
+
bind(EVENTS.CHANNEL_DELETED, () => {
|
|
167
198
|
callbacks.onChannelDeleted();
|
|
168
199
|
});
|
|
169
200
|
}
|
|
170
201
|
if (callbacks.onMessageRead) {
|
|
171
|
-
|
|
172
|
-
callbacks.onMessageRead(
|
|
202
|
+
bind(EVENTS.MESSAGE_READ, (payload) => {
|
|
203
|
+
callbacks.onMessageRead(
|
|
204
|
+
snakeToCamel(payload)
|
|
205
|
+
);
|
|
173
206
|
});
|
|
174
207
|
}
|
|
175
208
|
if (callbacks.onMessagePinned) {
|
|
176
|
-
|
|
209
|
+
bind(EVENTS.MESSAGE_PINNED, (payload) => {
|
|
177
210
|
callbacks.onMessagePinned(snakeToCamel(payload));
|
|
178
211
|
});
|
|
179
212
|
}
|
|
180
213
|
if (callbacks.onMessageUnpinned) {
|
|
181
|
-
|
|
214
|
+
bind(EVENTS.MESSAGE_UNPINNED, (payload) => {
|
|
182
215
|
callbacks.onMessageUnpinned(snakeToCamel(payload));
|
|
183
216
|
});
|
|
184
217
|
}
|
|
185
|
-
if (callbacks.onPresence) {
|
|
218
|
+
if (callbacks.onPresence && !entry.presence) {
|
|
186
219
|
const presence = new Presence(channel);
|
|
187
220
|
presence.onSync(() => {
|
|
188
221
|
const online = {};
|
|
@@ -191,27 +224,45 @@ var CollabSocket = class {
|
|
|
191
224
|
});
|
|
192
225
|
callbacks.onPresence(online);
|
|
193
226
|
});
|
|
227
|
+
entry.presence = presence;
|
|
194
228
|
this.presences.set(conversationId, presence);
|
|
195
229
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
230
|
+
entry.subscribers += 1;
|
|
231
|
+
let released = false;
|
|
232
|
+
const release = () => {
|
|
233
|
+
if (released) return;
|
|
234
|
+
released = true;
|
|
235
|
+
const current = this.channels.get(conversationId);
|
|
236
|
+
if (!current) return;
|
|
237
|
+
for (const { event, ref } of refs) {
|
|
238
|
+
current.channel.off(event, ref);
|
|
239
|
+
}
|
|
240
|
+
current.subscribers -= 1;
|
|
241
|
+
if (current.subscribers <= 0) {
|
|
242
|
+
current.channel.leave();
|
|
243
|
+
this.channels.delete(conversationId);
|
|
244
|
+
this.presences.delete(conversationId);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
return { channel, release };
|
|
206
248
|
}
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
/**
|
|
250
|
+
* @deprecated Use the `release()` method returned by `joinConversation()`.
|
|
251
|
+
* Kept as a no-op so older callers don't throw — but it cannot identify
|
|
252
|
+
* which subscriber should leave, so it silently does nothing. Any code
|
|
253
|
+
* still calling this will leak listeners and prevent the channel from
|
|
254
|
+
* ever being torn down. Migrate to the subscription handle.
|
|
255
|
+
*/
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
257
|
+
leaveConversation(_conversationId) {
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Look up the underlying Phoenix Channel for a conversation, if any
|
|
261
|
+
* subscriber is still holding it. All send/push paths go through this
|
|
262
|
+
* helper so the refcounted entry shape is contained to joinConversation.
|
|
263
|
+
*/
|
|
264
|
+
getChannel(conversationId) {
|
|
265
|
+
return this.channels.get(conversationId)?.channel ?? null;
|
|
215
266
|
}
|
|
216
267
|
/** Send a message to a conversation.
|
|
217
268
|
*
|
|
@@ -223,7 +274,7 @@ var CollabSocket = class {
|
|
|
223
274
|
*/
|
|
224
275
|
sendMessage(conversationId, payload) {
|
|
225
276
|
return new Promise((resolve, reject) => {
|
|
226
|
-
const channel = this.
|
|
277
|
+
const channel = this.getChannel(conversationId);
|
|
227
278
|
if (!channel) {
|
|
228
279
|
reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
|
|
229
280
|
return;
|
|
@@ -246,7 +297,7 @@ var CollabSocket = class {
|
|
|
246
297
|
}
|
|
247
298
|
/** Broadcast typing indicator */
|
|
248
299
|
sendTyping(conversationId, isTyping) {
|
|
249
|
-
const channel = this.
|
|
300
|
+
const channel = this.getChannel(conversationId);
|
|
250
301
|
channel?.push(EVENTS.USER_TYPING, {
|
|
251
302
|
userId: this.config?.userId,
|
|
252
303
|
userName: this.config?.userName,
|
|
@@ -255,7 +306,7 @@ var CollabSocket = class {
|
|
|
255
306
|
}
|
|
256
307
|
/** Mark messages as read */
|
|
257
308
|
markAsRead(conversationId, messageId) {
|
|
258
|
-
const channel = this.
|
|
309
|
+
const channel = this.getChannel(conversationId);
|
|
259
310
|
channel?.push(EVENTS.MESSAGE_READ, {
|
|
260
311
|
messageId,
|
|
261
312
|
userId: this.config?.userId
|
|
@@ -289,7 +340,7 @@ var CollabSocket = class {
|
|
|
289
340
|
/** Generic push with promise wrapper */
|
|
290
341
|
channelPush(conversationId, event, payload) {
|
|
291
342
|
return new Promise((resolve, reject) => {
|
|
292
|
-
const channel = this.
|
|
343
|
+
const channel = this.getChannel(conversationId);
|
|
293
344
|
if (!channel) {
|
|
294
345
|
reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
|
|
295
346
|
return;
|
|
@@ -306,7 +357,7 @@ var CollabSocket = class {
|
|
|
306
357
|
}
|
|
307
358
|
/** Disconnect socket and leave all channels */
|
|
308
359
|
disconnect() {
|
|
309
|
-
this.channels.forEach((
|
|
360
|
+
this.channels.forEach((entry) => entry.channel.leave());
|
|
310
361
|
this.channels.clear();
|
|
311
362
|
this.presences.clear();
|
|
312
363
|
this.userChannel?.leave();
|
|
@@ -361,7 +412,21 @@ var THEME_VAR = {
|
|
|
361
412
|
dangerBg: "--natoe-colab-danger-bg",
|
|
362
413
|
dangerBorder: "--natoe-colab-danger-border",
|
|
363
414
|
dangerFg: "--natoe-colab-danger-fg",
|
|
364
|
-
fontStack: "--natoe-colab-font-stack"
|
|
415
|
+
fontStack: "--natoe-colab-font-stack",
|
|
416
|
+
// Neutral surface palette — promoted to CSS vars so consumers (e.g.
|
|
417
|
+
// CollabPanel themeMode='dark') can flip the whole grayscale at a subtree
|
|
418
|
+
// level without re-themeing every component.
|
|
419
|
+
white: "--natoe-colab-white",
|
|
420
|
+
neutral50: "--natoe-colab-neutral-50",
|
|
421
|
+
neutral100: "--natoe-colab-neutral-100",
|
|
422
|
+
neutral200: "--natoe-colab-neutral-200",
|
|
423
|
+
neutral300: "--natoe-colab-neutral-300",
|
|
424
|
+
neutral400: "--natoe-colab-neutral-400",
|
|
425
|
+
neutral500: "--natoe-colab-neutral-500",
|
|
426
|
+
neutral600: "--natoe-colab-neutral-600",
|
|
427
|
+
neutral700: "--natoe-colab-neutral-700",
|
|
428
|
+
neutral800: "--natoe-colab-neutral-800",
|
|
429
|
+
neutral900: "--natoe-colab-neutral-900"
|
|
365
430
|
};
|
|
366
431
|
var THEME_DEFAULTS = {
|
|
367
432
|
primary: "#2563eb",
|
|
@@ -374,18 +439,9 @@ var THEME_DEFAULTS = {
|
|
|
374
439
|
dangerBg: "#fef2f2",
|
|
375
440
|
dangerBorder: "#fecaca",
|
|
376
441
|
dangerFg: "#b91c1c",
|
|
377
|
-
fontStack: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
var COLOR = {
|
|
381
|
-
// Brand — themeable
|
|
382
|
-
primary: cssVar(THEME_VAR.primary, THEME_DEFAULTS.primary),
|
|
383
|
-
primaryHover: cssVar(THEME_VAR.primaryHover, THEME_DEFAULTS.primaryHover),
|
|
384
|
-
/** Tinted surface for chips/cards on brand-coloured states. */
|
|
385
|
-
primaryBg: cssVar(THEME_VAR.primaryBg, THEME_DEFAULTS.primaryBg),
|
|
386
|
-
primaryFg: cssVar(THEME_VAR.primaryFg, THEME_DEFAULTS.primaryFg),
|
|
387
|
-
// Neutral grayscale — not themeable, taken from Tailwind's zinc-leaning
|
|
388
|
-
// slate to match the package's existing tonal balance.
|
|
442
|
+
fontStack: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
443
|
+
// Neutral grayscale — these defaults are the same hex literals the
|
|
444
|
+
// package shipped with; they're now overridable per-subtree.
|
|
389
445
|
white: "#ffffff",
|
|
390
446
|
neutral50: "#f9fafb",
|
|
391
447
|
neutral100: "#f3f4f6",
|
|
@@ -396,7 +452,32 @@ var COLOR = {
|
|
|
396
452
|
neutral600: "#4b5563",
|
|
397
453
|
neutral700: "#374151",
|
|
398
454
|
neutral800: "#1f2937",
|
|
399
|
-
neutral900: "#111827"
|
|
455
|
+
neutral900: "#111827"
|
|
456
|
+
};
|
|
457
|
+
var cssVar = (name, fallback) => `var(${name}, ${fallback})`;
|
|
458
|
+
var COLOR = {
|
|
459
|
+
// Brand — themeable
|
|
460
|
+
primary: cssVar(THEME_VAR.primary, THEME_DEFAULTS.primary),
|
|
461
|
+
primaryHover: cssVar(THEME_VAR.primaryHover, THEME_DEFAULTS.primaryHover),
|
|
462
|
+
/** Tinted surface for chips/cards on brand-coloured states. */
|
|
463
|
+
primaryBg: cssVar(THEME_VAR.primaryBg, THEME_DEFAULTS.primaryBg),
|
|
464
|
+
primaryFg: cssVar(THEME_VAR.primaryFg, THEME_DEFAULTS.primaryFg),
|
|
465
|
+
// Neutral grayscale — themeable via CSS variables. Light-mode defaults
|
|
466
|
+
// taken from Tailwind's zinc-leaning slate to match the package's
|
|
467
|
+
// existing tonal balance; a host can override the entire palette by
|
|
468
|
+
// setting --natoe-colab-* values on any ancestor element (used by
|
|
469
|
+
// CollabPanel themeMode='dark' for the viewer's left-panel surface).
|
|
470
|
+
white: cssVar(THEME_VAR.white, THEME_DEFAULTS.white),
|
|
471
|
+
neutral50: cssVar(THEME_VAR.neutral50, THEME_DEFAULTS.neutral50),
|
|
472
|
+
neutral100: cssVar(THEME_VAR.neutral100, THEME_DEFAULTS.neutral100),
|
|
473
|
+
neutral200: cssVar(THEME_VAR.neutral200, THEME_DEFAULTS.neutral200),
|
|
474
|
+
neutral300: cssVar(THEME_VAR.neutral300, THEME_DEFAULTS.neutral300),
|
|
475
|
+
neutral400: cssVar(THEME_VAR.neutral400, THEME_DEFAULTS.neutral400),
|
|
476
|
+
neutral500: cssVar(THEME_VAR.neutral500, THEME_DEFAULTS.neutral500),
|
|
477
|
+
neutral600: cssVar(THEME_VAR.neutral600, THEME_DEFAULTS.neutral600),
|
|
478
|
+
neutral700: cssVar(THEME_VAR.neutral700, THEME_DEFAULTS.neutral700),
|
|
479
|
+
neutral800: cssVar(THEME_VAR.neutral800, THEME_DEFAULTS.neutral800),
|
|
480
|
+
neutral900: cssVar(THEME_VAR.neutral900, THEME_DEFAULTS.neutral900),
|
|
400
481
|
slate800: "#1e293b",
|
|
401
482
|
// Semantic — themeable
|
|
402
483
|
success: cssVar(THEME_VAR.success, THEME_DEFAULTS.success),
|
|
@@ -775,6 +856,7 @@ function useConversation({
|
|
|
775
856
|
const [isConnected, setIsConnected] = useState(false);
|
|
776
857
|
const [replyTo, setReplyTo] = useState(null);
|
|
777
858
|
const joinedConversationId = useRef(null);
|
|
859
|
+
const channelSubscription = useRef(null);
|
|
778
860
|
const typingTimers = useRef(/* @__PURE__ */ new Map());
|
|
779
861
|
const ensureConversationInFlight = useRef(null);
|
|
780
862
|
const markedReadIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
@@ -788,7 +870,9 @@ function useConversation({
|
|
|
788
870
|
const joinChannel = useCallback(
|
|
789
871
|
(conv) => {
|
|
790
872
|
if (joinedConversationId.current === conv.id) return;
|
|
791
|
-
|
|
873
|
+
channelSubscription.current?.release();
|
|
874
|
+
channelSubscription.current = null;
|
|
875
|
+
const subscription = socket.joinConversation(conv.id, {
|
|
792
876
|
// Dedupe by id so an optimistic message (added client-side on send)
|
|
793
877
|
// doesn't double up when the server's broadcast arrives.
|
|
794
878
|
onMessage: (msg) => setMessages((prev) => {
|
|
@@ -838,6 +922,8 @@ function useConversation({
|
|
|
838
922
|
onChannelDeleted: () => {
|
|
839
923
|
setConversation(null);
|
|
840
924
|
setMessages([]);
|
|
925
|
+
channelSubscription.current?.release();
|
|
926
|
+
channelSubscription.current = null;
|
|
841
927
|
joinedConversationId.current = null;
|
|
842
928
|
setIsConnected(false);
|
|
843
929
|
},
|
|
@@ -863,6 +949,7 @@ function useConversation({
|
|
|
863
949
|
);
|
|
864
950
|
}
|
|
865
951
|
});
|
|
952
|
+
channelSubscription.current = subscription;
|
|
866
953
|
joinedConversationId.current = conv.id;
|
|
867
954
|
setIsConnected(true);
|
|
868
955
|
},
|
|
@@ -923,10 +1010,9 @@ function useConversation({
|
|
|
923
1010
|
init();
|
|
924
1011
|
return () => {
|
|
925
1012
|
cancelled = true;
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1013
|
+
channelSubscription.current?.release();
|
|
1014
|
+
channelSubscription.current = null;
|
|
1015
|
+
joinedConversationId.current = null;
|
|
930
1016
|
typingTimers.current.forEach((timer) => clearTimeout(timer));
|
|
931
1017
|
typingTimers.current.clear();
|
|
932
1018
|
markedReadIdsRef.current.clear();
|
|
@@ -1342,17 +1428,26 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
|
|
|
1342
1428
|
}
|
|
1343
1429
|
);
|
|
1344
1430
|
}
|
|
1345
|
-
function
|
|
1346
|
-
return /* @__PURE__ */
|
|
1431
|
+
function PeopleIcon({ size = 18, color = "currentColor" }) {
|
|
1432
|
+
return /* @__PURE__ */ jsxs(
|
|
1347
1433
|
"svg",
|
|
1348
1434
|
{
|
|
1349
1435
|
xmlns: "http://www.w3.org/2000/svg",
|
|
1350
1436
|
width: size,
|
|
1351
1437
|
height: size,
|
|
1352
1438
|
viewBox: "0 0 24 24",
|
|
1353
|
-
fill:
|
|
1439
|
+
fill: "none",
|
|
1440
|
+
stroke: color,
|
|
1441
|
+
strokeWidth: "2",
|
|
1442
|
+
strokeLinecap: "round",
|
|
1443
|
+
strokeLinejoin: "round",
|
|
1354
1444
|
"aria-hidden": "true",
|
|
1355
|
-
children:
|
|
1445
|
+
children: [
|
|
1446
|
+
/* @__PURE__ */ jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
|
|
1447
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
|
|
1448
|
+
/* @__PURE__ */ jsx("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
|
|
1449
|
+
/* @__PURE__ */ jsx("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
|
|
1450
|
+
]
|
|
1356
1451
|
}
|
|
1357
1452
|
);
|
|
1358
1453
|
}
|
|
@@ -1377,6 +1472,7 @@ function resolveDisplayName(patientData, displayName) {
|
|
|
1377
1472
|
}
|
|
1378
1473
|
function PatientHeader({
|
|
1379
1474
|
patientData,
|
|
1475
|
+
participants,
|
|
1380
1476
|
onOpenDicom,
|
|
1381
1477
|
onOpenSettings,
|
|
1382
1478
|
onBack,
|
|
@@ -1442,15 +1538,18 @@ function PatientHeader({
|
|
|
1442
1538
|
]
|
|
1443
1539
|
}
|
|
1444
1540
|
),
|
|
1445
|
-
onOpenSettings && /* @__PURE__ */
|
|
1541
|
+
onOpenSettings && /* @__PURE__ */ jsxs(
|
|
1446
1542
|
"button",
|
|
1447
1543
|
{
|
|
1448
1544
|
onClick: onOpenSettings,
|
|
1449
1545
|
style: styles.settingsIconButton,
|
|
1450
1546
|
type: "button",
|
|
1451
|
-
"aria-label":
|
|
1452
|
-
title: "
|
|
1453
|
-
children:
|
|
1547
|
+
"aria-label": `Open channel settings (${participants.length} participants)`,
|
|
1548
|
+
title: "Channel participants",
|
|
1549
|
+
children: [
|
|
1550
|
+
/* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
|
|
1551
|
+
/* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
|
|
1552
|
+
]
|
|
1454
1553
|
}
|
|
1455
1554
|
)
|
|
1456
1555
|
] })
|
|
@@ -1558,20 +1657,28 @@ var styles = {
|
|
|
1558
1657
|
borderRadius: RADIUS.lg,
|
|
1559
1658
|
cursor: "pointer"
|
|
1560
1659
|
},
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1660
|
+
// Channel-info button: people icon + participant count. Click opens the
|
|
1661
|
+
// channel settings overlay (kept on the same handler as before so hosts
|
|
1662
|
+
// don't have to re-wire — the affordance just looks like a "members"
|
|
1663
|
+
// pill now instead of a gear).
|
|
1563
1664
|
settingsIconButton: {
|
|
1564
|
-
|
|
1565
|
-
height: SIZE.control,
|
|
1665
|
+
minHeight: SIZE.control,
|
|
1566
1666
|
display: "inline-flex",
|
|
1567
1667
|
alignItems: "center",
|
|
1568
|
-
|
|
1668
|
+
gap: SPACE.S2,
|
|
1669
|
+
padding: `0 ${SPACE.S3}`,
|
|
1569
1670
|
backgroundColor: COLOR.white,
|
|
1570
1671
|
border: `1px solid ${COLOR.neutral200}`,
|
|
1571
1672
|
borderRadius: RADIUS.lg,
|
|
1572
1673
|
color: COLOR.neutral600,
|
|
1573
1674
|
cursor: "pointer",
|
|
1574
1675
|
flexShrink: 0
|
|
1676
|
+
},
|
|
1677
|
+
participantCount: {
|
|
1678
|
+
fontSize: FONT_SIZE.sm,
|
|
1679
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
1680
|
+
color: COLOR.neutral700,
|
|
1681
|
+
fontVariantNumeric: "tabular-nums"
|
|
1575
1682
|
}
|
|
1576
1683
|
};
|
|
1577
1684
|
function ReplyQuoteBlock({
|
|
@@ -3992,6 +4099,20 @@ var styles12 = {
|
|
|
3992
4099
|
}
|
|
3993
4100
|
};
|
|
3994
4101
|
ensureGlobalStyles();
|
|
4102
|
+
var DARK_THEME_OVERRIDES = {
|
|
4103
|
+
["--natoe-colab-white"]: "#0b0b0c",
|
|
4104
|
+
["--natoe-colab-neutral-50"]: "#18181c",
|
|
4105
|
+
["--natoe-colab-neutral-100"]: "#1f1f23",
|
|
4106
|
+
["--natoe-colab-neutral-200"]: "#2a2a2e",
|
|
4107
|
+
["--natoe-colab-neutral-300"]: "#3f3f44",
|
|
4108
|
+
["--natoe-colab-neutral-400"]: "#6b7280",
|
|
4109
|
+
["--natoe-colab-neutral-500"]: "#9ca3af",
|
|
4110
|
+
["--natoe-colab-neutral-600"]: "#cbd5e1",
|
|
4111
|
+
["--natoe-colab-neutral-700"]: "#e5e7eb",
|
|
4112
|
+
["--natoe-colab-neutral-800"]: "#f3f4f6",
|
|
4113
|
+
["--natoe-colab-neutral-900"]: "#ffffff",
|
|
4114
|
+
["--natoe-colab-primary-bg"]: "rgba(37, 99, 235, 0.18)"
|
|
4115
|
+
};
|
|
3995
4116
|
function CollabPanel({
|
|
3996
4117
|
orderId,
|
|
3997
4118
|
patientData,
|
|
@@ -4000,6 +4121,7 @@ function CollabPanel({
|
|
|
4000
4121
|
onBack,
|
|
4001
4122
|
hidePatientName = false,
|
|
4002
4123
|
onConversationChange,
|
|
4124
|
+
themeMode = "light",
|
|
4003
4125
|
className,
|
|
4004
4126
|
style
|
|
4005
4127
|
}) {
|
|
@@ -4075,16 +4197,21 @@ function CollabPanel({
|
|
|
4075
4197
|
}
|
|
4076
4198
|
};
|
|
4077
4199
|
const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
|
|
4200
|
+
const containerStyle = {
|
|
4201
|
+
...panelStyles.container,
|
|
4202
|
+
...themeMode === "dark" ? DARK_THEME_OVERRIDES : {},
|
|
4203
|
+
...style
|
|
4204
|
+
};
|
|
4078
4205
|
if (error) {
|
|
4079
|
-
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4206
|
+
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
|
|
4080
4207
|
/* @__PURE__ */ jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
|
|
4081
4208
|
/* @__PURE__ */ jsx("p", { style: panelStyles.errorMessage, children: error })
|
|
4082
4209
|
] }) });
|
|
4083
4210
|
}
|
|
4084
4211
|
if (isLoading && !conversation) {
|
|
4085
|
-
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4212
|
+
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
|
|
4086
4213
|
}
|
|
4087
|
-
return /* @__PURE__ */ jsxs("div", { className
|
|
4214
|
+
return /* @__PURE__ */ jsxs("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: [
|
|
4088
4215
|
showSettings && conversation ? /* @__PURE__ */ jsx(
|
|
4089
4216
|
ChannelSettings,
|
|
4090
4217
|
{
|
|
@@ -4520,6 +4647,7 @@ function useInlineCollab({
|
|
|
4520
4647
|
const elementRef = useRef(null);
|
|
4521
4648
|
const observerRef = useRef(null);
|
|
4522
4649
|
const subscribedConversationIdRef = useRef(null);
|
|
4650
|
+
const channelSubscriptionRef = useRef(null);
|
|
4523
4651
|
const trimToLimit = useCallback(
|
|
4524
4652
|
(msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
|
|
4525
4653
|
[messageLimit]
|
|
@@ -4557,7 +4685,9 @@ function useInlineCollab({
|
|
|
4557
4685
|
const subscribe = useCallback(
|
|
4558
4686
|
(conversationId) => {
|
|
4559
4687
|
if (subscribedConversationIdRef.current === conversationId) return;
|
|
4560
|
-
|
|
4688
|
+
channelSubscriptionRef.current?.release();
|
|
4689
|
+
channelSubscriptionRef.current = null;
|
|
4690
|
+
const subscription = socket.joinConversation(conversationId, {
|
|
4561
4691
|
onMessage: (msg) => {
|
|
4562
4692
|
setMessages((prev) => trimToLimit([...prev, msg]));
|
|
4563
4693
|
if (msg.senderId !== config.userId) {
|
|
@@ -4573,19 +4703,18 @@ function useInlineCollab({
|
|
|
4573
4703
|
setParticipants((prev) => prev.filter((p) => p.userId !== participant.userId));
|
|
4574
4704
|
}
|
|
4575
4705
|
});
|
|
4706
|
+
channelSubscriptionRef.current = subscription;
|
|
4576
4707
|
subscribedConversationIdRef.current = conversationId;
|
|
4577
4708
|
setIsSubscribed(true);
|
|
4578
4709
|
},
|
|
4579
4710
|
[socket, trimToLimit, config.userId]
|
|
4580
4711
|
);
|
|
4581
4712
|
const unsubscribe = useCallback(() => {
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
}
|
|
4588
|
-
}, [socket]);
|
|
4713
|
+
channelSubscriptionRef.current?.release();
|
|
4714
|
+
channelSubscriptionRef.current = null;
|
|
4715
|
+
subscribedConversationIdRef.current = null;
|
|
4716
|
+
setIsSubscribed(false);
|
|
4717
|
+
}, []);
|
|
4589
4718
|
const containerRef = useCallback(
|
|
4590
4719
|
(element) => {
|
|
4591
4720
|
if (observerRef.current) {
|
|
@@ -4727,16 +4856,52 @@ function useInlineCollab({
|
|
|
4727
4856
|
};
|
|
4728
4857
|
}
|
|
4729
4858
|
ensureGlobalStyles();
|
|
4859
|
+
function palette(mode) {
|
|
4860
|
+
if (mode === "dark") {
|
|
4861
|
+
return {
|
|
4862
|
+
bg: "transparent",
|
|
4863
|
+
border: "transparent",
|
|
4864
|
+
previewBorder: "#1f1f23",
|
|
4865
|
+
senderName: "#f3f4f6",
|
|
4866
|
+
messageText: "#cbd5e1",
|
|
4867
|
+
systemText: "#6b7280",
|
|
4868
|
+
inputBg: "#18181c",
|
|
4869
|
+
inputBorder: "#2a2a2e",
|
|
4870
|
+
inputText: "#e5e7eb",
|
|
4871
|
+
audioBg: "rgba(37, 99, 235, 0.18)",
|
|
4872
|
+
audioFg: "#93c5fd",
|
|
4873
|
+
expandBorder: "#2a2a2e",
|
|
4874
|
+
expandColor: "#9ca3af"
|
|
4875
|
+
};
|
|
4876
|
+
}
|
|
4877
|
+
return {
|
|
4878
|
+
bg: COLOR.white,
|
|
4879
|
+
border: COLOR.neutral200,
|
|
4880
|
+
previewBorder: COLOR.neutral100,
|
|
4881
|
+
senderName: COLOR.neutral700,
|
|
4882
|
+
messageText: COLOR.neutral600,
|
|
4883
|
+
systemText: COLOR.neutral400,
|
|
4884
|
+
inputBg: "transparent",
|
|
4885
|
+
inputBorder: COLOR.neutral200,
|
|
4886
|
+
inputText: COLOR.neutral900,
|
|
4887
|
+
audioBg: COLOR.primaryBg,
|
|
4888
|
+
audioFg: COLOR.primary,
|
|
4889
|
+
expandBorder: COLOR.neutral200,
|
|
4890
|
+
expandColor: COLOR.neutral500
|
|
4891
|
+
};
|
|
4892
|
+
}
|
|
4730
4893
|
function CollabInline({
|
|
4731
4894
|
orderId,
|
|
4732
4895
|
patientData,
|
|
4733
4896
|
participantIds,
|
|
4734
4897
|
onExpand,
|
|
4735
|
-
messageLimit =
|
|
4898
|
+
messageLimit = 1,
|
|
4736
4899
|
placeholder = "Type a message about this case...",
|
|
4900
|
+
mode = "light",
|
|
4737
4901
|
className,
|
|
4738
4902
|
style
|
|
4739
4903
|
}) {
|
|
4904
|
+
const pal = palette(mode);
|
|
4740
4905
|
const {
|
|
4741
4906
|
hasConversation,
|
|
4742
4907
|
messages,
|
|
@@ -4748,11 +4913,17 @@ function CollabInline({
|
|
|
4748
4913
|
sendAudioMessage,
|
|
4749
4914
|
containerRef
|
|
4750
4915
|
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
|
|
4916
|
+
const containerStyle = {
|
|
4917
|
+
...styles14.container,
|
|
4918
|
+
backgroundColor: pal.bg,
|
|
4919
|
+
borderColor: pal.border,
|
|
4920
|
+
...style
|
|
4921
|
+
};
|
|
4751
4922
|
if (isLoading && !hasConversation) {
|
|
4752
|
-
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4923
|
+
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
|
|
4753
4924
|
}
|
|
4754
|
-
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4755
|
-
hasConversation && messages.length > 0 && /* @__PURE__ */ jsx("div", { style: styles14.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsx(InlineMessageRow, { message }, message.id)) }),
|
|
4925
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: [
|
|
4926
|
+
hasConversation && messages.length > 0 && /* @__PURE__ */ jsx("div", { style: { ...styles14.preview, borderBottomColor: pal.previewBorder }, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsx(InlineMessageRow, { message, pal }, message.id)) }),
|
|
4756
4927
|
error && /* @__PURE__ */ jsx("div", { style: styles14.error, children: error }),
|
|
4757
4928
|
/* @__PURE__ */ jsx(
|
|
4758
4929
|
InlineInputBar,
|
|
@@ -4763,33 +4934,34 @@ function CollabInline({
|
|
|
4763
4934
|
unreadCount: hasConversation ? unreadCount : 0,
|
|
4764
4935
|
isLive: isSubscribed,
|
|
4765
4936
|
showExpand: hasConversation && !!onExpand,
|
|
4766
|
-
onExpand
|
|
4937
|
+
onExpand,
|
|
4938
|
+
pal
|
|
4767
4939
|
}
|
|
4768
4940
|
)
|
|
4769
4941
|
] });
|
|
4770
4942
|
}
|
|
4771
|
-
function InlineMessageRow({ message }) {
|
|
4943
|
+
function InlineMessageRow({ message, pal }) {
|
|
4772
4944
|
if (message.type === "system") {
|
|
4773
|
-
return /* @__PURE__ */ jsx("div", { style: styles14.systemRow, children: /* @__PURE__ */ jsx("span", { style: styles14.systemText, children: message.body }) });
|
|
4945
|
+
return /* @__PURE__ */ jsx("div", { style: styles14.systemRow, children: /* @__PURE__ */ jsx("span", { style: { ...styles14.systemText, color: pal.systemText }, children: message.body }) });
|
|
4774
4946
|
}
|
|
4775
4947
|
if (message.type === "audio" && message.mediaUrl) {
|
|
4776
4948
|
return /* @__PURE__ */ jsxs("div", { style: styles14.messageRow, children: [
|
|
4777
4949
|
/* @__PURE__ */ jsx(RoleDot, { role: message.senderRole }),
|
|
4778
|
-
/* @__PURE__ */ jsxs("span", { style: styles14.senderName, children: [
|
|
4950
|
+
/* @__PURE__ */ jsxs("span", { style: { ...styles14.senderName, color: pal.senderName }, children: [
|
|
4779
4951
|
message.senderName,
|
|
4780
4952
|
":"
|
|
4781
4953
|
] }),
|
|
4782
|
-
/* @__PURE__ */ jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration })
|
|
4954
|
+
/* @__PURE__ */ jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration, pal })
|
|
4783
4955
|
] });
|
|
4784
4956
|
}
|
|
4785
4957
|
const preview = renderMessagePreview(message);
|
|
4786
4958
|
return /* @__PURE__ */ jsxs("div", { style: styles14.messageRow, children: [
|
|
4787
4959
|
/* @__PURE__ */ jsx(RoleDot, { role: message.senderRole }),
|
|
4788
|
-
/* @__PURE__ */ jsxs("span", { style: styles14.senderName, children: [
|
|
4960
|
+
/* @__PURE__ */ jsxs("span", { style: { ...styles14.senderName, color: pal.senderName }, children: [
|
|
4789
4961
|
message.senderName,
|
|
4790
4962
|
":"
|
|
4791
4963
|
] }),
|
|
4792
|
-
/* @__PURE__ */ jsx("span", { style: styles14.messageText, children: preview })
|
|
4964
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.messageText, color: pal.messageText }, children: preview })
|
|
4793
4965
|
] });
|
|
4794
4966
|
}
|
|
4795
4967
|
function RoleDot({ role }) {
|
|
@@ -4805,7 +4977,11 @@ function RoleDot({ role }) {
|
|
|
4805
4977
|
};
|
|
4806
4978
|
return /* @__PURE__ */ jsx("span", { style: { ...styles14.roleDot, backgroundColor: colors[role] } });
|
|
4807
4979
|
}
|
|
4808
|
-
function InlineAudioPlayer({
|
|
4980
|
+
function InlineAudioPlayer({
|
|
4981
|
+
url,
|
|
4982
|
+
duration,
|
|
4983
|
+
pal
|
|
4984
|
+
}) {
|
|
4809
4985
|
const audioRef = useRef(null);
|
|
4810
4986
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
4811
4987
|
const [currentTime, setCurrentTime] = useState(0);
|
|
@@ -4847,7 +5023,7 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
4847
5023
|
const total = duration ?? 0;
|
|
4848
5024
|
const remaining = Math.max(0, total - Math.floor(currentTime));
|
|
4849
5025
|
const displayTime = isPlaying ? remaining : total;
|
|
4850
|
-
return /* @__PURE__ */ jsxs("span", { style: styles14.audioPlayer, children: [
|
|
5026
|
+
return /* @__PURE__ */ jsxs("span", { style: { ...styles14.audioPlayer, backgroundColor: pal.audioBg, borderColor: pal.audioBg }, children: [
|
|
4851
5027
|
/* @__PURE__ */ jsx(
|
|
4852
5028
|
"button",
|
|
4853
5029
|
{
|
|
@@ -4859,15 +5035,15 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
4859
5035
|
}
|
|
4860
5036
|
),
|
|
4861
5037
|
/* @__PURE__ */ jsxs("span", { style: styles14.audioWaveform, children: [
|
|
4862
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%" } }),
|
|
4863
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "80%" } }),
|
|
4864
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "60%" } }),
|
|
4865
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "90%" } }),
|
|
4866
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "50%" } }),
|
|
4867
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "70%" } }),
|
|
4868
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%" } })
|
|
5038
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } }),
|
|
5039
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "80%", backgroundColor: pal.audioFg } }),
|
|
5040
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "60%", backgroundColor: pal.audioFg } }),
|
|
5041
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "90%", backgroundColor: pal.audioFg } }),
|
|
5042
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "50%", backgroundColor: pal.audioFg } }),
|
|
5043
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "70%", backgroundColor: pal.audioFg } }),
|
|
5044
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } })
|
|
4869
5045
|
] }),
|
|
4870
|
-
/* @__PURE__ */ jsx("span", { style: styles14.audioDuration, children: formatDuration2(displayTime) }),
|
|
5046
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children: formatDuration2(displayTime) }),
|
|
4871
5047
|
/* @__PURE__ */ jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
|
|
4872
5048
|
] });
|
|
4873
5049
|
}
|
|
@@ -4897,7 +5073,8 @@ function InlineInputBar({
|
|
|
4897
5073
|
unreadCount,
|
|
4898
5074
|
isLive,
|
|
4899
5075
|
showExpand,
|
|
4900
|
-
onExpand
|
|
5076
|
+
onExpand,
|
|
5077
|
+
pal
|
|
4901
5078
|
}) {
|
|
4902
5079
|
const [text, setText] = useState("");
|
|
4903
5080
|
const [isSending, setIsSending] = useState(false);
|
|
@@ -4934,7 +5111,12 @@ function InlineInputBar({
|
|
|
4934
5111
|
onKeyDown: handleKeyDown,
|
|
4935
5112
|
placeholder,
|
|
4936
5113
|
disabled: isSending,
|
|
4937
|
-
style:
|
|
5114
|
+
style: {
|
|
5115
|
+
...styles14.input,
|
|
5116
|
+
backgroundColor: pal.inputBg,
|
|
5117
|
+
borderColor: pal.inputBorder,
|
|
5118
|
+
color: pal.inputText
|
|
5119
|
+
}
|
|
4938
5120
|
}
|
|
4939
5121
|
),
|
|
4940
5122
|
unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
|
|
@@ -4954,7 +5136,11 @@ function InlineInputBar({
|
|
|
4954
5136
|
"button",
|
|
4955
5137
|
{
|
|
4956
5138
|
onClick: onExpand,
|
|
4957
|
-
style:
|
|
5139
|
+
style: {
|
|
5140
|
+
...styles14.expandButton,
|
|
5141
|
+
borderColor: pal.expandBorder,
|
|
5142
|
+
color: pal.expandColor
|
|
5143
|
+
},
|
|
4958
5144
|
title: "Expand to full chat",
|
|
4959
5145
|
type: "button",
|
|
4960
5146
|
children: "\u26F6"
|
|
@@ -5501,8 +5687,11 @@ function ConversationList({
|
|
|
5501
5687
|
onSelect,
|
|
5502
5688
|
className
|
|
5503
5689
|
}) {
|
|
5690
|
+
const { totalUnread } = useCollab();
|
|
5504
5691
|
const [query, setQuery] = useState("");
|
|
5505
|
-
const [filter, setFilter] = useState(
|
|
5692
|
+
const [filter, setFilter] = useState(
|
|
5693
|
+
() => totalUnread > 0 ? "unread" : "all"
|
|
5694
|
+
);
|
|
5506
5695
|
const [searchFocused, setSearchFocused] = useState(false);
|
|
5507
5696
|
const unreadCount = useMemo(
|
|
5508
5697
|
() => conversations.filter((c) => c.unreadCount > 0).length,
|