@natoe/colab 0.1.13 → 0.1.16
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 +120 -14
- package/dist/index.d.ts +120 -14
- package/dist/index.js +317 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +317 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -57,9 +57,29 @@ function toCamelKey(key) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// src/core/socket.ts
|
|
60
|
+
function normalizeUnreadPayload(payload) {
|
|
61
|
+
if (payload && typeof payload === "object") {
|
|
62
|
+
const obj = payload;
|
|
63
|
+
if (obj.conversation && typeof obj.conversation === "object") {
|
|
64
|
+
return {
|
|
65
|
+
conversation: obj.conversation,
|
|
66
|
+
order: obj.order && typeof obj.order === "object" ? obj.order : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { conversation: payload, order: {} };
|
|
70
|
+
}
|
|
71
|
+
return { conversation: {}, order: {} };
|
|
72
|
+
}
|
|
60
73
|
var CollabSocket = class {
|
|
61
74
|
constructor() {
|
|
62
75
|
this.socket = null;
|
|
76
|
+
/**
|
|
77
|
+
* Conversation channels keyed by id. Each entry is reference-counted so
|
|
78
|
+
* multiple surfaces (e.g. inline chat + expanded panel mounted at once
|
|
79
|
+
* for the same conversation) can coexist without one's `leaveConversation`
|
|
80
|
+
* tearing the channel out from under the other. See `joinConversation`
|
|
81
|
+
* and the returned `ChannelSubscription.release`.
|
|
82
|
+
*/
|
|
63
83
|
this.channels = /* @__PURE__ */ new Map();
|
|
64
84
|
this.presences = /* @__PURE__ */ new Map();
|
|
65
85
|
this.userChannel = null;
|
|
@@ -115,7 +135,7 @@ var CollabSocket = class {
|
|
|
115
135
|
if (!this.socket || !this.config) return;
|
|
116
136
|
this.userChannel = this.socket.channel("user_notifications", {});
|
|
117
137
|
this.userChannel.on("unread_update", (payload) => {
|
|
118
|
-
this.onUnreadUpdate?.(payload);
|
|
138
|
+
this.onUnreadUpdate?.(normalizeUnreadPayload(payload));
|
|
119
139
|
});
|
|
120
140
|
this.userChannel.join().receive("ok", () => {
|
|
121
141
|
}).receive("error", (reason) => {
|
|
@@ -126,63 +146,91 @@ var CollabSocket = class {
|
|
|
126
146
|
});
|
|
127
147
|
});
|
|
128
148
|
}
|
|
129
|
-
/** Register callback for unread count changes
|
|
149
|
+
/** Register callback for unread count changes. The callback receives
|
|
150
|
+
* the normalised {@link UnreadCounts} shape regardless of which
|
|
151
|
+
* payload format the backend pushed. */
|
|
130
152
|
onUnreadCountUpdate(callback) {
|
|
131
153
|
this.onUnreadUpdate = callback;
|
|
132
154
|
}
|
|
133
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Join a conversation channel and subscribe to events.
|
|
157
|
+
*
|
|
158
|
+
* Reference-counted: multiple callers can join the same conversation
|
|
159
|
+
* (e.g. inline preview + expanded panel mounted side-by-side). Each
|
|
160
|
+
* call binds its own listeners and gets back a `ChannelSubscription`.
|
|
161
|
+
* The underlying channel only `.leave()`s the server when the LAST
|
|
162
|
+
* subscriber calls `release()`.
|
|
163
|
+
*/
|
|
134
164
|
joinConversation(conversationId, callbacks) {
|
|
135
165
|
if (!this.socket) return null;
|
|
136
|
-
|
|
137
|
-
|
|
166
|
+
let entry = this.channels.get(conversationId);
|
|
167
|
+
if (!entry) {
|
|
168
|
+
const channel2 = this.socket.channel(`conversation:${conversationId}`, {});
|
|
169
|
+
channel2.join().receive("ok", () => {
|
|
170
|
+
}).receive("error", (reason) => {
|
|
171
|
+
this.config?.onError?.({
|
|
172
|
+
code: "CHANNEL_JOIN_ERROR",
|
|
173
|
+
message: `Failed to join conversation ${conversationId}`,
|
|
174
|
+
details: reason
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
entry = { channel: channel2, subscribers: 0, presence: null };
|
|
178
|
+
this.channels.set(conversationId, entry);
|
|
138
179
|
}
|
|
139
|
-
const channel =
|
|
180
|
+
const channel = entry.channel;
|
|
181
|
+
const refs = [];
|
|
182
|
+
const bind = (event, fn) => {
|
|
183
|
+
const ref = channel.on(event, fn);
|
|
184
|
+
refs.push({ event, ref });
|
|
185
|
+
};
|
|
140
186
|
if (callbacks.onMessage) {
|
|
141
|
-
|
|
187
|
+
bind(EVENTS.MESSAGE_NEW, (payload) => {
|
|
142
188
|
callbacks.onMessage(snakeToCamel(payload));
|
|
143
189
|
});
|
|
144
190
|
}
|
|
145
191
|
if (callbacks.onTyping) {
|
|
146
|
-
|
|
192
|
+
bind(EVENTS.USER_TYPING, (payload) => {
|
|
147
193
|
callbacks.onTyping(snakeToCamel(payload));
|
|
148
194
|
});
|
|
149
195
|
}
|
|
150
196
|
if (callbacks.onUserJoined) {
|
|
151
|
-
|
|
197
|
+
bind(EVENTS.USER_JOINED, (payload) => {
|
|
152
198
|
callbacks.onUserJoined(snakeToCamel(payload));
|
|
153
199
|
});
|
|
154
200
|
}
|
|
155
201
|
if (callbacks.onUserLeft) {
|
|
156
|
-
|
|
202
|
+
bind(EVENTS.USER_LEFT, (payload) => {
|
|
157
203
|
callbacks.onUserLeft(snakeToCamel(payload));
|
|
158
204
|
});
|
|
159
205
|
}
|
|
160
206
|
if (callbacks.onChannelUpdated) {
|
|
161
|
-
|
|
207
|
+
bind(EVENTS.CHANNEL_UPDATED, (payload) => {
|
|
162
208
|
callbacks.onChannelUpdated(snakeToCamel(payload));
|
|
163
209
|
});
|
|
164
210
|
}
|
|
165
211
|
if (callbacks.onChannelDeleted) {
|
|
166
|
-
|
|
212
|
+
bind(EVENTS.CHANNEL_DELETED, () => {
|
|
167
213
|
callbacks.onChannelDeleted();
|
|
168
214
|
});
|
|
169
215
|
}
|
|
170
216
|
if (callbacks.onMessageRead) {
|
|
171
|
-
|
|
172
|
-
callbacks.onMessageRead(
|
|
217
|
+
bind(EVENTS.MESSAGE_READ, (payload) => {
|
|
218
|
+
callbacks.onMessageRead(
|
|
219
|
+
snakeToCamel(payload)
|
|
220
|
+
);
|
|
173
221
|
});
|
|
174
222
|
}
|
|
175
223
|
if (callbacks.onMessagePinned) {
|
|
176
|
-
|
|
224
|
+
bind(EVENTS.MESSAGE_PINNED, (payload) => {
|
|
177
225
|
callbacks.onMessagePinned(snakeToCamel(payload));
|
|
178
226
|
});
|
|
179
227
|
}
|
|
180
228
|
if (callbacks.onMessageUnpinned) {
|
|
181
|
-
|
|
229
|
+
bind(EVENTS.MESSAGE_UNPINNED, (payload) => {
|
|
182
230
|
callbacks.onMessageUnpinned(snakeToCamel(payload));
|
|
183
231
|
});
|
|
184
232
|
}
|
|
185
|
-
if (callbacks.onPresence) {
|
|
233
|
+
if (callbacks.onPresence && !entry.presence) {
|
|
186
234
|
const presence = new Presence(channel);
|
|
187
235
|
presence.onSync(() => {
|
|
188
236
|
const online = {};
|
|
@@ -191,27 +239,45 @@ var CollabSocket = class {
|
|
|
191
239
|
});
|
|
192
240
|
callbacks.onPresence(online);
|
|
193
241
|
});
|
|
242
|
+
entry.presence = presence;
|
|
194
243
|
this.presences.set(conversationId, presence);
|
|
195
244
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
245
|
+
entry.subscribers += 1;
|
|
246
|
+
let released = false;
|
|
247
|
+
const release = () => {
|
|
248
|
+
if (released) return;
|
|
249
|
+
released = true;
|
|
250
|
+
const current = this.channels.get(conversationId);
|
|
251
|
+
if (!current) return;
|
|
252
|
+
for (const { event, ref } of refs) {
|
|
253
|
+
current.channel.off(event, ref);
|
|
254
|
+
}
|
|
255
|
+
current.subscribers -= 1;
|
|
256
|
+
if (current.subscribers <= 0) {
|
|
257
|
+
current.channel.leave();
|
|
258
|
+
this.channels.delete(conversationId);
|
|
259
|
+
this.presences.delete(conversationId);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
return { channel, release };
|
|
206
263
|
}
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
264
|
+
/**
|
|
265
|
+
* @deprecated Use the `release()` method returned by `joinConversation()`.
|
|
266
|
+
* Kept as a no-op so older callers don't throw — but it cannot identify
|
|
267
|
+
* which subscriber should leave, so it silently does nothing. Any code
|
|
268
|
+
* still calling this will leak listeners and prevent the channel from
|
|
269
|
+
* ever being torn down. Migrate to the subscription handle.
|
|
270
|
+
*/
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
272
|
+
leaveConversation(_conversationId) {
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Look up the underlying Phoenix Channel for a conversation, if any
|
|
276
|
+
* subscriber is still holding it. All send/push paths go through this
|
|
277
|
+
* helper so the refcounted entry shape is contained to joinConversation.
|
|
278
|
+
*/
|
|
279
|
+
getChannel(conversationId) {
|
|
280
|
+
return this.channels.get(conversationId)?.channel ?? null;
|
|
215
281
|
}
|
|
216
282
|
/** Send a message to a conversation.
|
|
217
283
|
*
|
|
@@ -223,7 +289,7 @@ var CollabSocket = class {
|
|
|
223
289
|
*/
|
|
224
290
|
sendMessage(conversationId, payload) {
|
|
225
291
|
return new Promise((resolve, reject) => {
|
|
226
|
-
const channel = this.
|
|
292
|
+
const channel = this.getChannel(conversationId);
|
|
227
293
|
if (!channel) {
|
|
228
294
|
reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
|
|
229
295
|
return;
|
|
@@ -246,7 +312,7 @@ var CollabSocket = class {
|
|
|
246
312
|
}
|
|
247
313
|
/** Broadcast typing indicator */
|
|
248
314
|
sendTyping(conversationId, isTyping) {
|
|
249
|
-
const channel = this.
|
|
315
|
+
const channel = this.getChannel(conversationId);
|
|
250
316
|
channel?.push(EVENTS.USER_TYPING, {
|
|
251
317
|
userId: this.config?.userId,
|
|
252
318
|
userName: this.config?.userName,
|
|
@@ -255,7 +321,7 @@ var CollabSocket = class {
|
|
|
255
321
|
}
|
|
256
322
|
/** Mark messages as read */
|
|
257
323
|
markAsRead(conversationId, messageId) {
|
|
258
|
-
const channel = this.
|
|
324
|
+
const channel = this.getChannel(conversationId);
|
|
259
325
|
channel?.push(EVENTS.MESSAGE_READ, {
|
|
260
326
|
messageId,
|
|
261
327
|
userId: this.config?.userId
|
|
@@ -289,7 +355,7 @@ var CollabSocket = class {
|
|
|
289
355
|
/** Generic push with promise wrapper */
|
|
290
356
|
channelPush(conversationId, event, payload) {
|
|
291
357
|
return new Promise((resolve, reject) => {
|
|
292
|
-
const channel = this.
|
|
358
|
+
const channel = this.getChannel(conversationId);
|
|
293
359
|
if (!channel) {
|
|
294
360
|
reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
|
|
295
361
|
return;
|
|
@@ -306,7 +372,7 @@ var CollabSocket = class {
|
|
|
306
372
|
}
|
|
307
373
|
/** Disconnect socket and leave all channels */
|
|
308
374
|
disconnect() {
|
|
309
|
-
this.channels.forEach((
|
|
375
|
+
this.channels.forEach((entry) => entry.channel.leave());
|
|
310
376
|
this.channels.clear();
|
|
311
377
|
this.presences.clear();
|
|
312
378
|
this.userChannel?.leave();
|
|
@@ -361,7 +427,21 @@ var THEME_VAR = {
|
|
|
361
427
|
dangerBg: "--natoe-colab-danger-bg",
|
|
362
428
|
dangerBorder: "--natoe-colab-danger-border",
|
|
363
429
|
dangerFg: "--natoe-colab-danger-fg",
|
|
364
|
-
fontStack: "--natoe-colab-font-stack"
|
|
430
|
+
fontStack: "--natoe-colab-font-stack",
|
|
431
|
+
// Neutral surface palette — promoted to CSS vars so consumers (e.g.
|
|
432
|
+
// CollabPanel themeMode='dark') can flip the whole grayscale at a subtree
|
|
433
|
+
// level without re-themeing every component.
|
|
434
|
+
white: "--natoe-colab-white",
|
|
435
|
+
neutral50: "--natoe-colab-neutral-50",
|
|
436
|
+
neutral100: "--natoe-colab-neutral-100",
|
|
437
|
+
neutral200: "--natoe-colab-neutral-200",
|
|
438
|
+
neutral300: "--natoe-colab-neutral-300",
|
|
439
|
+
neutral400: "--natoe-colab-neutral-400",
|
|
440
|
+
neutral500: "--natoe-colab-neutral-500",
|
|
441
|
+
neutral600: "--natoe-colab-neutral-600",
|
|
442
|
+
neutral700: "--natoe-colab-neutral-700",
|
|
443
|
+
neutral800: "--natoe-colab-neutral-800",
|
|
444
|
+
neutral900: "--natoe-colab-neutral-900"
|
|
365
445
|
};
|
|
366
446
|
var THEME_DEFAULTS = {
|
|
367
447
|
primary: "#2563eb",
|
|
@@ -374,18 +454,9 @@ var THEME_DEFAULTS = {
|
|
|
374
454
|
dangerBg: "#fef2f2",
|
|
375
455
|
dangerBorder: "#fecaca",
|
|
376
456
|
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.
|
|
457
|
+
fontStack: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
458
|
+
// Neutral grayscale — these defaults are the same hex literals the
|
|
459
|
+
// package shipped with; they're now overridable per-subtree.
|
|
389
460
|
white: "#ffffff",
|
|
390
461
|
neutral50: "#f9fafb",
|
|
391
462
|
neutral100: "#f3f4f6",
|
|
@@ -396,7 +467,32 @@ var COLOR = {
|
|
|
396
467
|
neutral600: "#4b5563",
|
|
397
468
|
neutral700: "#374151",
|
|
398
469
|
neutral800: "#1f2937",
|
|
399
|
-
neutral900: "#111827"
|
|
470
|
+
neutral900: "#111827"
|
|
471
|
+
};
|
|
472
|
+
var cssVar = (name, fallback) => `var(${name}, ${fallback})`;
|
|
473
|
+
var COLOR = {
|
|
474
|
+
// Brand — themeable
|
|
475
|
+
primary: cssVar(THEME_VAR.primary, THEME_DEFAULTS.primary),
|
|
476
|
+
primaryHover: cssVar(THEME_VAR.primaryHover, THEME_DEFAULTS.primaryHover),
|
|
477
|
+
/** Tinted surface for chips/cards on brand-coloured states. */
|
|
478
|
+
primaryBg: cssVar(THEME_VAR.primaryBg, THEME_DEFAULTS.primaryBg),
|
|
479
|
+
primaryFg: cssVar(THEME_VAR.primaryFg, THEME_DEFAULTS.primaryFg),
|
|
480
|
+
// Neutral grayscale — themeable via CSS variables. Light-mode defaults
|
|
481
|
+
// taken from Tailwind's zinc-leaning slate to match the package's
|
|
482
|
+
// existing tonal balance; a host can override the entire palette by
|
|
483
|
+
// setting --natoe-colab-* values on any ancestor element (used by
|
|
484
|
+
// CollabPanel themeMode='dark' for the viewer's left-panel surface).
|
|
485
|
+
white: cssVar(THEME_VAR.white, THEME_DEFAULTS.white),
|
|
486
|
+
neutral50: cssVar(THEME_VAR.neutral50, THEME_DEFAULTS.neutral50),
|
|
487
|
+
neutral100: cssVar(THEME_VAR.neutral100, THEME_DEFAULTS.neutral100),
|
|
488
|
+
neutral200: cssVar(THEME_VAR.neutral200, THEME_DEFAULTS.neutral200),
|
|
489
|
+
neutral300: cssVar(THEME_VAR.neutral300, THEME_DEFAULTS.neutral300),
|
|
490
|
+
neutral400: cssVar(THEME_VAR.neutral400, THEME_DEFAULTS.neutral400),
|
|
491
|
+
neutral500: cssVar(THEME_VAR.neutral500, THEME_DEFAULTS.neutral500),
|
|
492
|
+
neutral600: cssVar(THEME_VAR.neutral600, THEME_DEFAULTS.neutral600),
|
|
493
|
+
neutral700: cssVar(THEME_VAR.neutral700, THEME_DEFAULTS.neutral700),
|
|
494
|
+
neutral800: cssVar(THEME_VAR.neutral800, THEME_DEFAULTS.neutral800),
|
|
495
|
+
neutral900: cssVar(THEME_VAR.neutral900, THEME_DEFAULTS.neutral900),
|
|
400
496
|
slate800: "#1e293b",
|
|
401
497
|
// Semantic — themeable
|
|
402
498
|
success: cssVar(THEME_VAR.success, THEME_DEFAULTS.success),
|
|
@@ -496,6 +592,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
496
592
|
return new CollabSocket();
|
|
497
593
|
});
|
|
498
594
|
const [unreadCounts, setUnreadCounts] = useState({});
|
|
595
|
+
const [unreadCountsByOrder, setUnreadCountsByOrder] = useState({});
|
|
499
596
|
const pendingOrderIds = useRef(/* @__PURE__ */ new Set());
|
|
500
597
|
const pendingResolvers = useRef(/* @__PURE__ */ new Map());
|
|
501
598
|
const previewCache = useRef(/* @__PURE__ */ new Map());
|
|
@@ -511,7 +608,8 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
511
608
|
setSocket(s);
|
|
512
609
|
}
|
|
513
610
|
s.onUnreadCountUpdate((counts) => {
|
|
514
|
-
setUnreadCounts(counts);
|
|
611
|
+
setUnreadCounts(counts.conversation);
|
|
612
|
+
setUnreadCountsByOrder(counts.order);
|
|
515
613
|
previewCache.current.clear();
|
|
516
614
|
});
|
|
517
615
|
if (!s.isConnected()) {
|
|
@@ -719,6 +817,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
719
817
|
apiBaseUrl,
|
|
720
818
|
totalUnread,
|
|
721
819
|
unreadCounts,
|
|
820
|
+
unreadCountsByOrder,
|
|
722
821
|
requestPreview,
|
|
723
822
|
invalidatePreview,
|
|
724
823
|
fetchMessages,
|
|
@@ -775,6 +874,7 @@ function useConversation({
|
|
|
775
874
|
const [isConnected, setIsConnected] = useState(false);
|
|
776
875
|
const [replyTo, setReplyTo] = useState(null);
|
|
777
876
|
const joinedConversationId = useRef(null);
|
|
877
|
+
const channelSubscription = useRef(null);
|
|
778
878
|
const typingTimers = useRef(/* @__PURE__ */ new Map());
|
|
779
879
|
const ensureConversationInFlight = useRef(null);
|
|
780
880
|
const markedReadIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
@@ -788,7 +888,9 @@ function useConversation({
|
|
|
788
888
|
const joinChannel = useCallback(
|
|
789
889
|
(conv) => {
|
|
790
890
|
if (joinedConversationId.current === conv.id) return;
|
|
791
|
-
|
|
891
|
+
channelSubscription.current?.release();
|
|
892
|
+
channelSubscription.current = null;
|
|
893
|
+
const subscription = socket.joinConversation(conv.id, {
|
|
792
894
|
// Dedupe by id so an optimistic message (added client-side on send)
|
|
793
895
|
// doesn't double up when the server's broadcast arrives.
|
|
794
896
|
onMessage: (msg) => setMessages((prev) => {
|
|
@@ -838,6 +940,8 @@ function useConversation({
|
|
|
838
940
|
onChannelDeleted: () => {
|
|
839
941
|
setConversation(null);
|
|
840
942
|
setMessages([]);
|
|
943
|
+
channelSubscription.current?.release();
|
|
944
|
+
channelSubscription.current = null;
|
|
841
945
|
joinedConversationId.current = null;
|
|
842
946
|
setIsConnected(false);
|
|
843
947
|
},
|
|
@@ -863,6 +967,7 @@ function useConversation({
|
|
|
863
967
|
);
|
|
864
968
|
}
|
|
865
969
|
});
|
|
970
|
+
channelSubscription.current = subscription;
|
|
866
971
|
joinedConversationId.current = conv.id;
|
|
867
972
|
setIsConnected(true);
|
|
868
973
|
},
|
|
@@ -923,10 +1028,9 @@ function useConversation({
|
|
|
923
1028
|
init();
|
|
924
1029
|
return () => {
|
|
925
1030
|
cancelled = true;
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1031
|
+
channelSubscription.current?.release();
|
|
1032
|
+
channelSubscription.current = null;
|
|
1033
|
+
joinedConversationId.current = null;
|
|
930
1034
|
typingTimers.current.forEach((timer) => clearTimeout(timer));
|
|
931
1035
|
typingTimers.current.clear();
|
|
932
1036
|
markedReadIdsRef.current.clear();
|
|
@@ -1342,17 +1446,26 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
|
|
|
1342
1446
|
}
|
|
1343
1447
|
);
|
|
1344
1448
|
}
|
|
1345
|
-
function
|
|
1346
|
-
return /* @__PURE__ */
|
|
1449
|
+
function PeopleIcon({ size = 18, color = "currentColor" }) {
|
|
1450
|
+
return /* @__PURE__ */ jsxs(
|
|
1347
1451
|
"svg",
|
|
1348
1452
|
{
|
|
1349
1453
|
xmlns: "http://www.w3.org/2000/svg",
|
|
1350
1454
|
width: size,
|
|
1351
1455
|
height: size,
|
|
1352
1456
|
viewBox: "0 0 24 24",
|
|
1353
|
-
fill:
|
|
1457
|
+
fill: "none",
|
|
1458
|
+
stroke: color,
|
|
1459
|
+
strokeWidth: "2",
|
|
1460
|
+
strokeLinecap: "round",
|
|
1461
|
+
strokeLinejoin: "round",
|
|
1354
1462
|
"aria-hidden": "true",
|
|
1355
|
-
children:
|
|
1463
|
+
children: [
|
|
1464
|
+
/* @__PURE__ */ jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
|
|
1465
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
|
|
1466
|
+
/* @__PURE__ */ jsx("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
|
|
1467
|
+
/* @__PURE__ */ jsx("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
|
|
1468
|
+
]
|
|
1356
1469
|
}
|
|
1357
1470
|
);
|
|
1358
1471
|
}
|
|
@@ -1377,6 +1490,7 @@ function resolveDisplayName(patientData, displayName) {
|
|
|
1377
1490
|
}
|
|
1378
1491
|
function PatientHeader({
|
|
1379
1492
|
patientData,
|
|
1493
|
+
participants,
|
|
1380
1494
|
onOpenDicom,
|
|
1381
1495
|
onOpenSettings,
|
|
1382
1496
|
onBack,
|
|
@@ -1442,15 +1556,18 @@ function PatientHeader({
|
|
|
1442
1556
|
]
|
|
1443
1557
|
}
|
|
1444
1558
|
),
|
|
1445
|
-
onOpenSettings && /* @__PURE__ */
|
|
1559
|
+
onOpenSettings && /* @__PURE__ */ jsxs(
|
|
1446
1560
|
"button",
|
|
1447
1561
|
{
|
|
1448
1562
|
onClick: onOpenSettings,
|
|
1449
1563
|
style: styles.settingsIconButton,
|
|
1450
1564
|
type: "button",
|
|
1451
|
-
"aria-label":
|
|
1452
|
-
title: "
|
|
1453
|
-
children:
|
|
1565
|
+
"aria-label": `Open channel settings (${participants.length} participants)`,
|
|
1566
|
+
title: "Channel participants",
|
|
1567
|
+
children: [
|
|
1568
|
+
/* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
|
|
1569
|
+
/* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
|
|
1570
|
+
]
|
|
1454
1571
|
}
|
|
1455
1572
|
)
|
|
1456
1573
|
] })
|
|
@@ -1558,20 +1675,28 @@ var styles = {
|
|
|
1558
1675
|
borderRadius: RADIUS.lg,
|
|
1559
1676
|
cursor: "pointer"
|
|
1560
1677
|
},
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1678
|
+
// Channel-info button: people icon + participant count. Click opens the
|
|
1679
|
+
// channel settings overlay (kept on the same handler as before so hosts
|
|
1680
|
+
// don't have to re-wire — the affordance just looks like a "members"
|
|
1681
|
+
// pill now instead of a gear).
|
|
1563
1682
|
settingsIconButton: {
|
|
1564
|
-
|
|
1565
|
-
height: SIZE.control,
|
|
1683
|
+
minHeight: SIZE.control,
|
|
1566
1684
|
display: "inline-flex",
|
|
1567
1685
|
alignItems: "center",
|
|
1568
|
-
|
|
1686
|
+
gap: SPACE.S2,
|
|
1687
|
+
padding: `0 ${SPACE.S3}`,
|
|
1569
1688
|
backgroundColor: COLOR.white,
|
|
1570
1689
|
border: `1px solid ${COLOR.neutral200}`,
|
|
1571
1690
|
borderRadius: RADIUS.lg,
|
|
1572
1691
|
color: COLOR.neutral600,
|
|
1573
1692
|
cursor: "pointer",
|
|
1574
1693
|
flexShrink: 0
|
|
1694
|
+
},
|
|
1695
|
+
participantCount: {
|
|
1696
|
+
fontSize: FONT_SIZE.sm,
|
|
1697
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
1698
|
+
color: COLOR.neutral700,
|
|
1699
|
+
fontVariantNumeric: "tabular-nums"
|
|
1575
1700
|
}
|
|
1576
1701
|
};
|
|
1577
1702
|
function ReplyQuoteBlock({
|
|
@@ -3992,6 +4117,20 @@ var styles12 = {
|
|
|
3992
4117
|
}
|
|
3993
4118
|
};
|
|
3994
4119
|
ensureGlobalStyles();
|
|
4120
|
+
var DARK_THEME_OVERRIDES = {
|
|
4121
|
+
["--natoe-colab-white"]: "#0b0b0c",
|
|
4122
|
+
["--natoe-colab-neutral-50"]: "#18181c",
|
|
4123
|
+
["--natoe-colab-neutral-100"]: "#1f1f23",
|
|
4124
|
+
["--natoe-colab-neutral-200"]: "#2a2a2e",
|
|
4125
|
+
["--natoe-colab-neutral-300"]: "#3f3f44",
|
|
4126
|
+
["--natoe-colab-neutral-400"]: "#6b7280",
|
|
4127
|
+
["--natoe-colab-neutral-500"]: "#9ca3af",
|
|
4128
|
+
["--natoe-colab-neutral-600"]: "#cbd5e1",
|
|
4129
|
+
["--natoe-colab-neutral-700"]: "#e5e7eb",
|
|
4130
|
+
["--natoe-colab-neutral-800"]: "#f3f4f6",
|
|
4131
|
+
["--natoe-colab-neutral-900"]: "#ffffff",
|
|
4132
|
+
["--natoe-colab-primary-bg"]: "rgba(37, 99, 235, 0.18)"
|
|
4133
|
+
};
|
|
3995
4134
|
function CollabPanel({
|
|
3996
4135
|
orderId,
|
|
3997
4136
|
patientData,
|
|
@@ -4000,6 +4139,7 @@ function CollabPanel({
|
|
|
4000
4139
|
onBack,
|
|
4001
4140
|
hidePatientName = false,
|
|
4002
4141
|
onConversationChange,
|
|
4142
|
+
themeMode = "light",
|
|
4003
4143
|
className,
|
|
4004
4144
|
style
|
|
4005
4145
|
}) {
|
|
@@ -4075,16 +4215,21 @@ function CollabPanel({
|
|
|
4075
4215
|
}
|
|
4076
4216
|
};
|
|
4077
4217
|
const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
|
|
4218
|
+
const containerStyle = {
|
|
4219
|
+
...panelStyles.container,
|
|
4220
|
+
...themeMode === "dark" ? DARK_THEME_OVERRIDES : {},
|
|
4221
|
+
...style
|
|
4222
|
+
};
|
|
4078
4223
|
if (error) {
|
|
4079
|
-
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4224
|
+
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
|
|
4080
4225
|
/* @__PURE__ */ jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
|
|
4081
4226
|
/* @__PURE__ */ jsx("p", { style: panelStyles.errorMessage, children: error })
|
|
4082
4227
|
] }) });
|
|
4083
4228
|
}
|
|
4084
4229
|
if (isLoading && !conversation) {
|
|
4085
|
-
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4230
|
+
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
|
|
4086
4231
|
}
|
|
4087
|
-
return /* @__PURE__ */ jsxs("div", { className
|
|
4232
|
+
return /* @__PURE__ */ jsxs("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: [
|
|
4088
4233
|
showSettings && conversation ? /* @__PURE__ */ jsx(
|
|
4089
4234
|
ChannelSettings,
|
|
4090
4235
|
{
|
|
@@ -4520,6 +4665,7 @@ function useInlineCollab({
|
|
|
4520
4665
|
const elementRef = useRef(null);
|
|
4521
4666
|
const observerRef = useRef(null);
|
|
4522
4667
|
const subscribedConversationIdRef = useRef(null);
|
|
4668
|
+
const channelSubscriptionRef = useRef(null);
|
|
4523
4669
|
const trimToLimit = useCallback(
|
|
4524
4670
|
(msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
|
|
4525
4671
|
[messageLimit]
|
|
@@ -4557,7 +4703,9 @@ function useInlineCollab({
|
|
|
4557
4703
|
const subscribe = useCallback(
|
|
4558
4704
|
(conversationId) => {
|
|
4559
4705
|
if (subscribedConversationIdRef.current === conversationId) return;
|
|
4560
|
-
|
|
4706
|
+
channelSubscriptionRef.current?.release();
|
|
4707
|
+
channelSubscriptionRef.current = null;
|
|
4708
|
+
const subscription = socket.joinConversation(conversationId, {
|
|
4561
4709
|
onMessage: (msg) => {
|
|
4562
4710
|
setMessages((prev) => trimToLimit([...prev, msg]));
|
|
4563
4711
|
if (msg.senderId !== config.userId) {
|
|
@@ -4573,19 +4721,18 @@ function useInlineCollab({
|
|
|
4573
4721
|
setParticipants((prev) => prev.filter((p) => p.userId !== participant.userId));
|
|
4574
4722
|
}
|
|
4575
4723
|
});
|
|
4724
|
+
channelSubscriptionRef.current = subscription;
|
|
4576
4725
|
subscribedConversationIdRef.current = conversationId;
|
|
4577
4726
|
setIsSubscribed(true);
|
|
4578
4727
|
},
|
|
4579
4728
|
[socket, trimToLimit, config.userId]
|
|
4580
4729
|
);
|
|
4581
4730
|
const unsubscribe = useCallback(() => {
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
}
|
|
4588
|
-
}, [socket]);
|
|
4731
|
+
channelSubscriptionRef.current?.release();
|
|
4732
|
+
channelSubscriptionRef.current = null;
|
|
4733
|
+
subscribedConversationIdRef.current = null;
|
|
4734
|
+
setIsSubscribed(false);
|
|
4735
|
+
}, []);
|
|
4589
4736
|
const containerRef = useCallback(
|
|
4590
4737
|
(element) => {
|
|
4591
4738
|
if (observerRef.current) {
|
|
@@ -4727,16 +4874,52 @@ function useInlineCollab({
|
|
|
4727
4874
|
};
|
|
4728
4875
|
}
|
|
4729
4876
|
ensureGlobalStyles();
|
|
4877
|
+
function palette(mode) {
|
|
4878
|
+
if (mode === "dark") {
|
|
4879
|
+
return {
|
|
4880
|
+
bg: "transparent",
|
|
4881
|
+
border: "transparent",
|
|
4882
|
+
previewBorder: "#1f1f23",
|
|
4883
|
+
senderName: "#f3f4f6",
|
|
4884
|
+
messageText: "#cbd5e1",
|
|
4885
|
+
systemText: "#6b7280",
|
|
4886
|
+
inputBg: "#18181c",
|
|
4887
|
+
inputBorder: "#2a2a2e",
|
|
4888
|
+
inputText: "#e5e7eb",
|
|
4889
|
+
audioBg: "rgba(37, 99, 235, 0.18)",
|
|
4890
|
+
audioFg: "#93c5fd",
|
|
4891
|
+
expandBorder: "#2a2a2e",
|
|
4892
|
+
expandColor: "#9ca3af"
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
return {
|
|
4896
|
+
bg: COLOR.white,
|
|
4897
|
+
border: COLOR.neutral200,
|
|
4898
|
+
previewBorder: COLOR.neutral100,
|
|
4899
|
+
senderName: COLOR.neutral700,
|
|
4900
|
+
messageText: COLOR.neutral600,
|
|
4901
|
+
systemText: COLOR.neutral400,
|
|
4902
|
+
inputBg: "transparent",
|
|
4903
|
+
inputBorder: COLOR.neutral200,
|
|
4904
|
+
inputText: COLOR.neutral900,
|
|
4905
|
+
audioBg: COLOR.primaryBg,
|
|
4906
|
+
audioFg: COLOR.primary,
|
|
4907
|
+
expandBorder: COLOR.neutral200,
|
|
4908
|
+
expandColor: COLOR.neutral500
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4730
4911
|
function CollabInline({
|
|
4731
4912
|
orderId,
|
|
4732
4913
|
patientData,
|
|
4733
4914
|
participantIds,
|
|
4734
4915
|
onExpand,
|
|
4735
|
-
messageLimit =
|
|
4916
|
+
messageLimit = 1,
|
|
4736
4917
|
placeholder = "Type a message about this case...",
|
|
4918
|
+
mode = "light",
|
|
4737
4919
|
className,
|
|
4738
4920
|
style
|
|
4739
4921
|
}) {
|
|
4922
|
+
const pal = palette(mode);
|
|
4740
4923
|
const {
|
|
4741
4924
|
hasConversation,
|
|
4742
4925
|
messages,
|
|
@@ -4748,11 +4931,17 @@ function CollabInline({
|
|
|
4748
4931
|
sendAudioMessage,
|
|
4749
4932
|
containerRef
|
|
4750
4933
|
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
|
|
4934
|
+
const containerStyle = {
|
|
4935
|
+
...styles14.container,
|
|
4936
|
+
backgroundColor: pal.bg,
|
|
4937
|
+
borderColor: pal.border,
|
|
4938
|
+
...style
|
|
4939
|
+
};
|
|
4751
4940
|
if (isLoading && !hasConversation) {
|
|
4752
|
-
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style:
|
|
4941
|
+
return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
|
|
4753
4942
|
}
|
|
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)) }),
|
|
4943
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: [
|
|
4944
|
+
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
4945
|
error && /* @__PURE__ */ jsx("div", { style: styles14.error, children: error }),
|
|
4757
4946
|
/* @__PURE__ */ jsx(
|
|
4758
4947
|
InlineInputBar,
|
|
@@ -4763,33 +4952,34 @@ function CollabInline({
|
|
|
4763
4952
|
unreadCount: hasConversation ? unreadCount : 0,
|
|
4764
4953
|
isLive: isSubscribed,
|
|
4765
4954
|
showExpand: hasConversation && !!onExpand,
|
|
4766
|
-
onExpand
|
|
4955
|
+
onExpand,
|
|
4956
|
+
pal
|
|
4767
4957
|
}
|
|
4768
4958
|
)
|
|
4769
4959
|
] });
|
|
4770
4960
|
}
|
|
4771
|
-
function InlineMessageRow({ message }) {
|
|
4961
|
+
function InlineMessageRow({ message, pal }) {
|
|
4772
4962
|
if (message.type === "system") {
|
|
4773
|
-
return /* @__PURE__ */ jsx("div", { style: styles14.systemRow, children: /* @__PURE__ */ jsx("span", { style: styles14.systemText, children: message.body }) });
|
|
4963
|
+
return /* @__PURE__ */ jsx("div", { style: styles14.systemRow, children: /* @__PURE__ */ jsx("span", { style: { ...styles14.systemText, color: pal.systemText }, children: message.body }) });
|
|
4774
4964
|
}
|
|
4775
4965
|
if (message.type === "audio" && message.mediaUrl) {
|
|
4776
4966
|
return /* @__PURE__ */ jsxs("div", { style: styles14.messageRow, children: [
|
|
4777
4967
|
/* @__PURE__ */ jsx(RoleDot, { role: message.senderRole }),
|
|
4778
|
-
/* @__PURE__ */ jsxs("span", { style: styles14.senderName, children: [
|
|
4968
|
+
/* @__PURE__ */ jsxs("span", { style: { ...styles14.senderName, color: pal.senderName }, children: [
|
|
4779
4969
|
message.senderName,
|
|
4780
4970
|
":"
|
|
4781
4971
|
] }),
|
|
4782
|
-
/* @__PURE__ */ jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration })
|
|
4972
|
+
/* @__PURE__ */ jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration, pal })
|
|
4783
4973
|
] });
|
|
4784
4974
|
}
|
|
4785
4975
|
const preview = renderMessagePreview(message);
|
|
4786
4976
|
return /* @__PURE__ */ jsxs("div", { style: styles14.messageRow, children: [
|
|
4787
4977
|
/* @__PURE__ */ jsx(RoleDot, { role: message.senderRole }),
|
|
4788
|
-
/* @__PURE__ */ jsxs("span", { style: styles14.senderName, children: [
|
|
4978
|
+
/* @__PURE__ */ jsxs("span", { style: { ...styles14.senderName, color: pal.senderName }, children: [
|
|
4789
4979
|
message.senderName,
|
|
4790
4980
|
":"
|
|
4791
4981
|
] }),
|
|
4792
|
-
/* @__PURE__ */ jsx("span", { style: styles14.messageText, children: preview })
|
|
4982
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.messageText, color: pal.messageText }, children: preview })
|
|
4793
4983
|
] });
|
|
4794
4984
|
}
|
|
4795
4985
|
function RoleDot({ role }) {
|
|
@@ -4805,7 +4995,11 @@ function RoleDot({ role }) {
|
|
|
4805
4995
|
};
|
|
4806
4996
|
return /* @__PURE__ */ jsx("span", { style: { ...styles14.roleDot, backgroundColor: colors[role] } });
|
|
4807
4997
|
}
|
|
4808
|
-
function InlineAudioPlayer({
|
|
4998
|
+
function InlineAudioPlayer({
|
|
4999
|
+
url,
|
|
5000
|
+
duration,
|
|
5001
|
+
pal
|
|
5002
|
+
}) {
|
|
4809
5003
|
const audioRef = useRef(null);
|
|
4810
5004
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
4811
5005
|
const [currentTime, setCurrentTime] = useState(0);
|
|
@@ -4847,7 +5041,7 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
4847
5041
|
const total = duration ?? 0;
|
|
4848
5042
|
const remaining = Math.max(0, total - Math.floor(currentTime));
|
|
4849
5043
|
const displayTime = isPlaying ? remaining : total;
|
|
4850
|
-
return /* @__PURE__ */ jsxs("span", { style: styles14.audioPlayer, children: [
|
|
5044
|
+
return /* @__PURE__ */ jsxs("span", { style: { ...styles14.audioPlayer, backgroundColor: pal.audioBg, borderColor: pal.audioBg }, children: [
|
|
4851
5045
|
/* @__PURE__ */ jsx(
|
|
4852
5046
|
"button",
|
|
4853
5047
|
{
|
|
@@ -4859,15 +5053,15 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
4859
5053
|
}
|
|
4860
5054
|
),
|
|
4861
5055
|
/* @__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%" } })
|
|
5056
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } }),
|
|
5057
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "80%", backgroundColor: pal.audioFg } }),
|
|
5058
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "60%", backgroundColor: pal.audioFg } }),
|
|
5059
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "90%", backgroundColor: pal.audioFg } }),
|
|
5060
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "50%", backgroundColor: pal.audioFg } }),
|
|
5061
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "70%", backgroundColor: pal.audioFg } }),
|
|
5062
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } })
|
|
4869
5063
|
] }),
|
|
4870
|
-
/* @__PURE__ */ jsx("span", { style: styles14.audioDuration, children: formatDuration2(displayTime) }),
|
|
5064
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children: formatDuration2(displayTime) }),
|
|
4871
5065
|
/* @__PURE__ */ jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
|
|
4872
5066
|
] });
|
|
4873
5067
|
}
|
|
@@ -4897,7 +5091,8 @@ function InlineInputBar({
|
|
|
4897
5091
|
unreadCount,
|
|
4898
5092
|
isLive,
|
|
4899
5093
|
showExpand,
|
|
4900
|
-
onExpand
|
|
5094
|
+
onExpand,
|
|
5095
|
+
pal
|
|
4901
5096
|
}) {
|
|
4902
5097
|
const [text, setText] = useState("");
|
|
4903
5098
|
const [isSending, setIsSending] = useState(false);
|
|
@@ -4934,7 +5129,12 @@ function InlineInputBar({
|
|
|
4934
5129
|
onKeyDown: handleKeyDown,
|
|
4935
5130
|
placeholder,
|
|
4936
5131
|
disabled: isSending,
|
|
4937
|
-
style:
|
|
5132
|
+
style: {
|
|
5133
|
+
...styles14.input,
|
|
5134
|
+
backgroundColor: pal.inputBg,
|
|
5135
|
+
borderColor: pal.inputBorder,
|
|
5136
|
+
color: pal.inputText
|
|
5137
|
+
}
|
|
4938
5138
|
}
|
|
4939
5139
|
),
|
|
4940
5140
|
unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
|
|
@@ -4954,7 +5154,11 @@ function InlineInputBar({
|
|
|
4954
5154
|
"button",
|
|
4955
5155
|
{
|
|
4956
5156
|
onClick: onExpand,
|
|
4957
|
-
style:
|
|
5157
|
+
style: {
|
|
5158
|
+
...styles14.expandButton,
|
|
5159
|
+
borderColor: pal.expandBorder,
|
|
5160
|
+
color: pal.expandColor
|
|
5161
|
+
},
|
|
4958
5162
|
title: "Expand to full chat",
|
|
4959
5163
|
type: "button",
|
|
4960
5164
|
children: "\u26F6"
|
|
@@ -5501,8 +5705,11 @@ function ConversationList({
|
|
|
5501
5705
|
onSelect,
|
|
5502
5706
|
className
|
|
5503
5707
|
}) {
|
|
5708
|
+
const { totalUnread } = useCollab();
|
|
5504
5709
|
const [query, setQuery] = useState("");
|
|
5505
|
-
const [filter, setFilter] = useState(
|
|
5710
|
+
const [filter, setFilter] = useState(
|
|
5711
|
+
() => totalUnread > 0 ? "unread" : "all"
|
|
5712
|
+
);
|
|
5506
5713
|
const [searchFocused, setSearchFocused] = useState(false);
|
|
5507
5714
|
const unreadCount = useMemo(
|
|
5508
5715
|
() => conversations.filter((c) => c.unreadCount > 0).length,
|
|
@@ -6132,7 +6339,7 @@ function useUnreadCount() {
|
|
|
6132
6339
|
const [counts, setCounts] = useState({});
|
|
6133
6340
|
useEffect(() => {
|
|
6134
6341
|
socket.onUnreadCountUpdate((serverCounts) => {
|
|
6135
|
-
setCounts(serverCounts);
|
|
6342
|
+
setCounts(serverCounts.conversation);
|
|
6136
6343
|
});
|
|
6137
6344
|
}, [socket]);
|
|
6138
6345
|
const getCountForConversation = useCallback(
|