@natoe/colab 0.1.18 → 0.1.20

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 CHANGED
@@ -964,7 +964,7 @@ declare function MessageBubble({ message, isOwn, participants, currentUserId, sh
964
964
 
965
965
  interface MessageInputProps {
966
966
  onSendText: (body: string) => Promise<void>;
967
- /** @deprecated MIC DISABLED kept in the prop signature for backward compat. */
967
+ /** Voice message handler. Receives the recorded blob + duration in seconds. */
968
968
  onSendAudio?: (blob: Blob, duration: number) => Promise<void>;
969
969
  onSendFile: (file: File) => Promise<void>;
970
970
  onTyping: (isTyping: boolean) => void;
@@ -976,7 +976,7 @@ interface MessageInputProps {
976
976
  placeholder?: string;
977
977
  className?: string;
978
978
  }
979
- declare function MessageInput({ onSendText, onSendFile, onTyping, replyTo, onCancelReply, disabled, placeholder, className, }: MessageInputProps): react_jsx_runtime.JSX.Element;
979
+ declare function MessageInput({ onSendText, onSendAudio, onSendFile, onTyping, replyTo, onCancelReply, disabled, placeholder, className, }: MessageInputProps): react_jsx_runtime.JSX.Element;
980
980
 
981
981
  interface MessageActionsMenuProps {
982
982
  message: Message;
package/dist/index.d.ts CHANGED
@@ -964,7 +964,7 @@ declare function MessageBubble({ message, isOwn, participants, currentUserId, sh
964
964
 
965
965
  interface MessageInputProps {
966
966
  onSendText: (body: string) => Promise<void>;
967
- /** @deprecated MIC DISABLED kept in the prop signature for backward compat. */
967
+ /** Voice message handler. Receives the recorded blob + duration in seconds. */
968
968
  onSendAudio?: (blob: Blob, duration: number) => Promise<void>;
969
969
  onSendFile: (file: File) => Promise<void>;
970
970
  onTyping: (isTyping: boolean) => void;
@@ -976,7 +976,7 @@ interface MessageInputProps {
976
976
  placeholder?: string;
977
977
  className?: string;
978
978
  }
979
- declare function MessageInput({ onSendText, onSendFile, onTyping, replyTo, onCancelReply, disabled, placeholder, className, }: MessageInputProps): react_jsx_runtime.JSX.Element;
979
+ declare function MessageInput({ onSendText, onSendAudio, onSendFile, onTyping, replyTo, onCancelReply, disabled, placeholder, className, }: MessageInputProps): react_jsx_runtime.JSX.Element;
980
980
 
981
981
  interface MessageActionsMenuProps {
982
982
  message: Message;
package/dist/index.js CHANGED
@@ -3094,6 +3094,121 @@ var styles6 = {
3094
3094
  animation: "natoe-colab-message-in 180ms ease-out"
3095
3095
  }
3096
3096
  };
3097
+ function useAudioRecorder() {
3098
+ const [isRecording, setIsRecording] = React4.useState(false);
3099
+ const [duration, setDuration] = React4.useState(0);
3100
+ const [error, setError] = React4.useState(null);
3101
+ const mediaRecorderRef = React4.useRef(null);
3102
+ const chunksRef = React4.useRef([]);
3103
+ const streamRef = React4.useRef(null);
3104
+ const timerRef = React4.useRef(null);
3105
+ const startTimeRef = React4.useRef(0);
3106
+ const resolveRef = React4.useRef(null);
3107
+ const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
3108
+ const cleanup = React4.useCallback(() => {
3109
+ if (timerRef.current) {
3110
+ clearInterval(timerRef.current);
3111
+ timerRef.current = null;
3112
+ }
3113
+ if (streamRef.current) {
3114
+ streamRef.current.getTracks().forEach((track) => track.stop());
3115
+ streamRef.current = null;
3116
+ }
3117
+ mediaRecorderRef.current = null;
3118
+ chunksRef.current = [];
3119
+ setIsRecording(false);
3120
+ setDuration(0);
3121
+ }, []);
3122
+ const start = React4.useCallback(async () => {
3123
+ if (!isSupported) {
3124
+ setError("Audio recording is not supported in this browser");
3125
+ return;
3126
+ }
3127
+ try {
3128
+ setError(null);
3129
+ chunksRef.current = [];
3130
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
3131
+ streamRef.current = stream;
3132
+ const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
3133
+ const recorder = new MediaRecorder(stream, { mimeType });
3134
+ mediaRecorderRef.current = recorder;
3135
+ recorder.ondataavailable = (event) => {
3136
+ if (event.data.size > 0) {
3137
+ chunksRef.current.push(event.data);
3138
+ }
3139
+ };
3140
+ recorder.onerror = () => {
3141
+ setError("Recording failed");
3142
+ cleanup();
3143
+ resolveRef.current?.(null);
3144
+ resolveRef.current = null;
3145
+ };
3146
+ recorder.onstop = () => {
3147
+ const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
3148
+ const blob = new Blob(chunksRef.current, { type: mimeType });
3149
+ cleanup();
3150
+ resolveRef.current?.({ blob, duration: finalDuration });
3151
+ resolveRef.current = null;
3152
+ };
3153
+ recorder.start(250);
3154
+ startTimeRef.current = Date.now();
3155
+ setIsRecording(true);
3156
+ timerRef.current = setInterval(() => {
3157
+ const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
3158
+ setDuration(elapsed);
3159
+ }, 1e3);
3160
+ } catch (err) {
3161
+ if (err instanceof DOMException && err.name === "NotAllowedError") {
3162
+ setError("Microphone access denied. Please allow microphone permissions.");
3163
+ } else {
3164
+ setError("Failed to start recording");
3165
+ }
3166
+ cleanup();
3167
+ }
3168
+ }, [isSupported, cleanup]);
3169
+ const stop = React4.useCallback(async () => {
3170
+ return new Promise((resolve) => {
3171
+ if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
3172
+ resolve(null);
3173
+ return;
3174
+ }
3175
+ resolveRef.current = resolve;
3176
+ mediaRecorderRef.current.stop();
3177
+ });
3178
+ }, []);
3179
+ const cancel = React4.useCallback(() => {
3180
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
3181
+ mediaRecorderRef.current.onstop = null;
3182
+ mediaRecorderRef.current.stop();
3183
+ }
3184
+ cleanup();
3185
+ resolveRef.current?.(null);
3186
+ resolveRef.current = null;
3187
+ }, [cleanup]);
3188
+ return {
3189
+ isRecording,
3190
+ duration,
3191
+ start,
3192
+ stop,
3193
+ cancel,
3194
+ isSupported,
3195
+ error
3196
+ };
3197
+ }
3198
+ function MicIcon({ size = 20, color = "currentColor" }) {
3199
+ return /* @__PURE__ */ jsxRuntime.jsx(
3200
+ "svg",
3201
+ {
3202
+ xmlns: "http://www.w3.org/2000/svg",
3203
+ width: size,
3204
+ height: size,
3205
+ viewBox: "0 0 24 24",
3206
+ fill: color,
3207
+ "aria-hidden": "true",
3208
+ 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" })
3209
+ }
3210
+ );
3211
+ }
3097
3212
  function ReplyPreview({ message, onCancel, className }) {
3098
3213
  const preview = renderQuotedPreview(message);
3099
3214
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles7.container, children: [
@@ -3230,8 +3345,7 @@ function CloseIcon({ size = 18, color = "currentColor" }) {
3230
3345
  }
3231
3346
  function MessageInput({
3232
3347
  onSendText,
3233
- // MIC DISABLED — onSendAudio still accepted but unused.
3234
- // onSendAudio,
3348
+ onSendAudio,
3235
3349
  onSendFile,
3236
3350
  onTyping,
3237
3351
  replyTo,
@@ -3248,6 +3362,15 @@ function MessageInput({
3248
3362
  const typingTimeoutRef = React4.useRef(null);
3249
3363
  const isTypingRef = React4.useRef(false);
3250
3364
  const sendingRef = React4.useRef(false);
3365
+ const {
3366
+ isRecording,
3367
+ duration,
3368
+ start: startRecording,
3369
+ stop: stopRecording,
3370
+ cancel: cancelRecording,
3371
+ isSupported: micSupported,
3372
+ error: micError
3373
+ } = useAudioRecorder();
3251
3374
  const handleTyping = React4.useCallback(() => {
3252
3375
  if (!isTypingRef.current) {
3253
3376
  isTypingRef.current = true;
@@ -3296,6 +3419,16 @@ function MessageInput({
3296
3419
  },
3297
3420
  [handleSendText]
3298
3421
  );
3422
+ const handleAudioStop = React4.useCallback(async () => {
3423
+ const result = await stopRecording();
3424
+ if (!result || !onSendAudio) return;
3425
+ setIsSending(true);
3426
+ try {
3427
+ await onSendAudio(result.blob, result.duration);
3428
+ } finally {
3429
+ setIsSending(false);
3430
+ }
3431
+ }, [stopRecording, onSendAudio]);
3299
3432
  const handleFileSelect = React4.useCallback(
3300
3433
  async (e) => {
3301
3434
  const file = e.target.files?.[0];
@@ -3336,7 +3469,18 @@ function MessageInput({
3336
3469
  }
3337
3470
  )
3338
3471
  ] }),
3339
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.inputBar, children: [
3472
+ micError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles8.error, role: "alert", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.errorText, children: micError }) }),
3473
+ isRecording ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingBar, children: [
3474
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingIndicator, children: [
3475
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.recordingDot }),
3476
+ "Recording ",
3477
+ formatDuration2(duration)
3478
+ ] }),
3479
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingActions, children: [
3480
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: cancelRecording, style: styles8.cancelButton, type: "button", children: "Cancel" }),
3481
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleAudioStop, style: styles8.stopButton, type: "button", children: "Send" })
3482
+ ] })
3483
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.inputBar, children: [
3340
3484
  /* @__PURE__ */ jsxRuntime.jsx(
3341
3485
  "button",
3342
3486
  {
@@ -3355,6 +3499,7 @@ function MessageInput({
3355
3499
  ref: fileInputRef,
3356
3500
  type: "file",
3357
3501
  onChange: handleFileSelect,
3502
+ "aria-label": "Choose a file to upload",
3358
3503
  style: { display: "none" },
3359
3504
  accept: [
3360
3505
  ...SUPPORTED_IMAGE_TYPES,
@@ -3385,7 +3530,19 @@ function MessageInput({
3385
3530
  style: styles8.textarea
3386
3531
  }
3387
3532
  ),
3388
- /* @__PURE__ */ jsxRuntime.jsx(
3533
+ onSendAudio && micSupported && !text.trim() && /* @__PURE__ */ jsxRuntime.jsx(
3534
+ "button",
3535
+ {
3536
+ onClick: startRecording,
3537
+ disabled: disabled || isSending,
3538
+ "aria-label": "Record voice message",
3539
+ title: "Record voice message",
3540
+ style: styles8.micButton,
3541
+ type: "button",
3542
+ children: /* @__PURE__ */ jsxRuntime.jsx(MicIcon, { size: 22, color: COLOR.neutral700 })
3543
+ }
3544
+ ),
3545
+ text.trim() && /* @__PURE__ */ jsxRuntime.jsx(
3389
3546
  "button",
3390
3547
  {
3391
3548
  onClick: handleSendText,
@@ -3403,6 +3560,11 @@ function MessageInput({
3403
3560
  ] })
3404
3561
  ] });
3405
3562
  }
3563
+ function formatDuration2(seconds) {
3564
+ const m = Math.floor(seconds / 60);
3565
+ const s = seconds % 60;
3566
+ return `${m}:${s.toString().padStart(2, "0")}`;
3567
+ }
3406
3568
  var styles8 = {
3407
3569
  container: {
3408
3570
  borderTop: `1px solid ${COLOR.neutral200}`,
@@ -3479,6 +3641,23 @@ var styles8 = {
3479
3641
  color: COLOR.neutral700,
3480
3642
  transition: "background-color 120ms ease, color 120ms ease, border-color 120ms ease"
3481
3643
  },
3644
+ // Same 48-square chrome as attach. Sits between the textarea and send
3645
+ // button when there's no draft text — replaces send while idle so the
3646
+ // input row stays one row tall.
3647
+ micButton: {
3648
+ width: "48px",
3649
+ height: "48px",
3650
+ display: "flex",
3651
+ alignItems: "center",
3652
+ justifyContent: "center",
3653
+ backgroundColor: COLOR.neutral100,
3654
+ border: `1.5px solid ${COLOR.neutral200}`,
3655
+ borderRadius: "14px",
3656
+ cursor: "pointer",
3657
+ flexShrink: 0,
3658
+ color: COLOR.neutral700,
3659
+ transition: "background-color 120ms ease, color 120ms ease, border-color 120ms ease"
3660
+ },
3482
3661
  textarea: {
3483
3662
  flex: 1,
3484
3663
  minHeight: "48px",
@@ -3511,7 +3690,53 @@ var styles8 = {
3511
3690
  cursor: "pointer",
3512
3691
  flexShrink: 0,
3513
3692
  transition: "opacity 120ms ease, background-color 120ms ease"
3514
- }};
3693
+ },
3694
+ recordingBar: {
3695
+ display: "flex",
3696
+ justifyContent: "space-between",
3697
+ alignItems: "center",
3698
+ padding: `${SPACE.S3} ${SPACE.S4}`,
3699
+ backgroundColor: COLOR.dangerBg
3700
+ },
3701
+ recordingIndicator: {
3702
+ display: "flex",
3703
+ alignItems: "center",
3704
+ gap: SPACE.S2,
3705
+ fontSize: FONT_SIZE.sm,
3706
+ color: COLOR.danger,
3707
+ fontWeight: FONT_WEIGHT.medium
3708
+ },
3709
+ recordingDot: {
3710
+ width: SPACE.S2,
3711
+ height: SPACE.S2,
3712
+ borderRadius: RADIUS.full,
3713
+ backgroundColor: COLOR.danger,
3714
+ animation: "pulse 1.5s infinite"
3715
+ },
3716
+ recordingActions: {
3717
+ display: "flex",
3718
+ gap: SPACE.S2
3719
+ },
3720
+ cancelButton: {
3721
+ padding: `${SPACE.S1} ${SPACE.S4}`,
3722
+ fontSize: FONT_SIZE.sm,
3723
+ color: COLOR.neutral500,
3724
+ backgroundColor: COLOR.white,
3725
+ border: `1px solid ${COLOR.neutral300}`,
3726
+ borderRadius: RADIUS.md,
3727
+ cursor: "pointer"
3728
+ },
3729
+ stopButton: {
3730
+ padding: `${SPACE.S1} ${SPACE.S4}`,
3731
+ fontSize: FONT_SIZE.sm,
3732
+ fontWeight: FONT_WEIGHT.medium,
3733
+ color: COLOR.white,
3734
+ backgroundColor: COLOR.danger,
3735
+ border: "none",
3736
+ borderRadius: RADIUS.md,
3737
+ cursor: "pointer"
3738
+ }
3739
+ };
3515
3740
  var ROLE_AVATAR_BG = {
3516
3741
  radiologist: "#4f46e5",
3517
3742
  // indigo
@@ -4938,6 +5163,7 @@ function useInlineCollab({
4938
5163
  }
4939
5164
  try {
4940
5165
  await socket.sendMessage(preview.conversationId, payload);
5166
+ setUnreadCount(0);
4941
5167
  } catch (err) {
4942
5168
  setError(err instanceof Error ? err.message : "Failed to send message");
4943
5169
  config.onError?.({
@@ -5193,7 +5419,7 @@ function InlineAudioPlayer({
5193
5419
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles14.waveBar, height: "70%", backgroundColor: pal.audioFg } }),
5194
5420
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } })
5195
5421
  ] }),
5196
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children: formatDuration2(displayTime) }),
5422
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children: formatDuration3(displayTime) }),
5197
5423
  /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
5198
5424
  ] });
5199
5425
  }
@@ -5209,7 +5435,7 @@ function renderMessagePreview(message) {
5209
5435
  return message.body;
5210
5436
  }
5211
5437
  }
5212
- function formatDuration2(seconds) {
5438
+ function formatDuration3(seconds) {
5213
5439
  const safe = Math.max(0, Math.floor(seconds));
5214
5440
  const m = Math.floor(safe / 60);
5215
5441
  const s = safe % 60;
@@ -5269,8 +5495,6 @@ function InlineInputBar({
5269
5495
  }
5270
5496
  }
5271
5497
  ),
5272
- unreadCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
5273
- isLive && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles14.liveDot, title: "Live updates active" }),
5274
5498
  text.trim() && /* @__PURE__ */ jsxRuntime.jsx(
5275
5499
  "button",
5276
5500
  {
@@ -5282,6 +5506,8 @@ function InlineInputBar({
5282
5506
  children: "\u27A4"
5283
5507
  }
5284
5508
  ),
5509
+ unreadCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
5510
+ isLive && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles14.liveDot, title: "Live updates active" }),
5285
5511
  showExpand && onExpand && /* @__PURE__ */ jsxRuntime.jsx(
5286
5512
  "button",
5287
5513
  {
@@ -6365,107 +6591,6 @@ function useMessages({
6365
6591
  reset
6366
6592
  };
6367
6593
  }
6368
- function useAudioRecorder() {
6369
- const [isRecording, setIsRecording] = React4.useState(false);
6370
- const [duration, setDuration] = React4.useState(0);
6371
- const [error, setError] = React4.useState(null);
6372
- const mediaRecorderRef = React4.useRef(null);
6373
- const chunksRef = React4.useRef([]);
6374
- const streamRef = React4.useRef(null);
6375
- const timerRef = React4.useRef(null);
6376
- const startTimeRef = React4.useRef(0);
6377
- const resolveRef = React4.useRef(null);
6378
- const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
6379
- const cleanup = React4.useCallback(() => {
6380
- if (timerRef.current) {
6381
- clearInterval(timerRef.current);
6382
- timerRef.current = null;
6383
- }
6384
- if (streamRef.current) {
6385
- streamRef.current.getTracks().forEach((track) => track.stop());
6386
- streamRef.current = null;
6387
- }
6388
- mediaRecorderRef.current = null;
6389
- chunksRef.current = [];
6390
- setIsRecording(false);
6391
- setDuration(0);
6392
- }, []);
6393
- const start = React4.useCallback(async () => {
6394
- if (!isSupported) {
6395
- setError("Audio recording is not supported in this browser");
6396
- return;
6397
- }
6398
- try {
6399
- setError(null);
6400
- chunksRef.current = [];
6401
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
6402
- streamRef.current = stream;
6403
- const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
6404
- const recorder = new MediaRecorder(stream, { mimeType });
6405
- mediaRecorderRef.current = recorder;
6406
- recorder.ondataavailable = (event) => {
6407
- if (event.data.size > 0) {
6408
- chunksRef.current.push(event.data);
6409
- }
6410
- };
6411
- recorder.onerror = () => {
6412
- setError("Recording failed");
6413
- cleanup();
6414
- resolveRef.current?.(null);
6415
- resolveRef.current = null;
6416
- };
6417
- recorder.onstop = () => {
6418
- const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
6419
- const blob = new Blob(chunksRef.current, { type: mimeType });
6420
- cleanup();
6421
- resolveRef.current?.({ blob, duration: finalDuration });
6422
- resolveRef.current = null;
6423
- };
6424
- recorder.start(250);
6425
- startTimeRef.current = Date.now();
6426
- setIsRecording(true);
6427
- timerRef.current = setInterval(() => {
6428
- const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
6429
- setDuration(elapsed);
6430
- }, 1e3);
6431
- } catch (err) {
6432
- if (err instanceof DOMException && err.name === "NotAllowedError") {
6433
- setError("Microphone access denied. Please allow microphone permissions.");
6434
- } else {
6435
- setError("Failed to start recording");
6436
- }
6437
- cleanup();
6438
- }
6439
- }, [isSupported, cleanup]);
6440
- const stop = React4.useCallback(async () => {
6441
- return new Promise((resolve) => {
6442
- if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
6443
- resolve(null);
6444
- return;
6445
- }
6446
- resolveRef.current = resolve;
6447
- mediaRecorderRef.current.stop();
6448
- });
6449
- }, []);
6450
- const cancel = React4.useCallback(() => {
6451
- if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
6452
- mediaRecorderRef.current.onstop = null;
6453
- mediaRecorderRef.current.stop();
6454
- }
6455
- cleanup();
6456
- resolveRef.current?.(null);
6457
- resolveRef.current = null;
6458
- }, [cleanup]);
6459
- return {
6460
- isRecording,
6461
- duration,
6462
- start,
6463
- stop,
6464
- cancel,
6465
- isSupported,
6466
- error
6467
- };
6468
- }
6469
6594
  function useUnreadCount() {
6470
6595
  const { socket, totalUnread } = useCollab();
6471
6596
  const [counts, setCounts] = React4.useState({});