@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 +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +235 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3088,6 +3088,121 @@ var styles6 = {
|
|
|
3088
3088
|
animation: "natoe-colab-message-in 180ms ease-out"
|
|
3089
3089
|
}
|
|
3090
3090
|
};
|
|
3091
|
+
function useAudioRecorder() {
|
|
3092
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
3093
|
+
const [duration, setDuration] = useState(0);
|
|
3094
|
+
const [error, setError] = useState(null);
|
|
3095
|
+
const mediaRecorderRef = useRef(null);
|
|
3096
|
+
const chunksRef = useRef([]);
|
|
3097
|
+
const streamRef = useRef(null);
|
|
3098
|
+
const timerRef = useRef(null);
|
|
3099
|
+
const startTimeRef = useRef(0);
|
|
3100
|
+
const resolveRef = useRef(null);
|
|
3101
|
+
const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
|
|
3102
|
+
const cleanup = useCallback(() => {
|
|
3103
|
+
if (timerRef.current) {
|
|
3104
|
+
clearInterval(timerRef.current);
|
|
3105
|
+
timerRef.current = null;
|
|
3106
|
+
}
|
|
3107
|
+
if (streamRef.current) {
|
|
3108
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
3109
|
+
streamRef.current = null;
|
|
3110
|
+
}
|
|
3111
|
+
mediaRecorderRef.current = null;
|
|
3112
|
+
chunksRef.current = [];
|
|
3113
|
+
setIsRecording(false);
|
|
3114
|
+
setDuration(0);
|
|
3115
|
+
}, []);
|
|
3116
|
+
const start = useCallback(async () => {
|
|
3117
|
+
if (!isSupported) {
|
|
3118
|
+
setError("Audio recording is not supported in this browser");
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
try {
|
|
3122
|
+
setError(null);
|
|
3123
|
+
chunksRef.current = [];
|
|
3124
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
3125
|
+
streamRef.current = stream;
|
|
3126
|
+
const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
|
|
3127
|
+
const recorder = new MediaRecorder(stream, { mimeType });
|
|
3128
|
+
mediaRecorderRef.current = recorder;
|
|
3129
|
+
recorder.ondataavailable = (event) => {
|
|
3130
|
+
if (event.data.size > 0) {
|
|
3131
|
+
chunksRef.current.push(event.data);
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
recorder.onerror = () => {
|
|
3135
|
+
setError("Recording failed");
|
|
3136
|
+
cleanup();
|
|
3137
|
+
resolveRef.current?.(null);
|
|
3138
|
+
resolveRef.current = null;
|
|
3139
|
+
};
|
|
3140
|
+
recorder.onstop = () => {
|
|
3141
|
+
const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
3142
|
+
const blob = new Blob(chunksRef.current, { type: mimeType });
|
|
3143
|
+
cleanup();
|
|
3144
|
+
resolveRef.current?.({ blob, duration: finalDuration });
|
|
3145
|
+
resolveRef.current = null;
|
|
3146
|
+
};
|
|
3147
|
+
recorder.start(250);
|
|
3148
|
+
startTimeRef.current = Date.now();
|
|
3149
|
+
setIsRecording(true);
|
|
3150
|
+
timerRef.current = setInterval(() => {
|
|
3151
|
+
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
3152
|
+
setDuration(elapsed);
|
|
3153
|
+
}, 1e3);
|
|
3154
|
+
} catch (err) {
|
|
3155
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
3156
|
+
setError("Microphone access denied. Please allow microphone permissions.");
|
|
3157
|
+
} else {
|
|
3158
|
+
setError("Failed to start recording");
|
|
3159
|
+
}
|
|
3160
|
+
cleanup();
|
|
3161
|
+
}
|
|
3162
|
+
}, [isSupported, cleanup]);
|
|
3163
|
+
const stop = useCallback(async () => {
|
|
3164
|
+
return new Promise((resolve) => {
|
|
3165
|
+
if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
|
|
3166
|
+
resolve(null);
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
resolveRef.current = resolve;
|
|
3170
|
+
mediaRecorderRef.current.stop();
|
|
3171
|
+
});
|
|
3172
|
+
}, []);
|
|
3173
|
+
const cancel = useCallback(() => {
|
|
3174
|
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
3175
|
+
mediaRecorderRef.current.onstop = null;
|
|
3176
|
+
mediaRecorderRef.current.stop();
|
|
3177
|
+
}
|
|
3178
|
+
cleanup();
|
|
3179
|
+
resolveRef.current?.(null);
|
|
3180
|
+
resolveRef.current = null;
|
|
3181
|
+
}, [cleanup]);
|
|
3182
|
+
return {
|
|
3183
|
+
isRecording,
|
|
3184
|
+
duration,
|
|
3185
|
+
start,
|
|
3186
|
+
stop,
|
|
3187
|
+
cancel,
|
|
3188
|
+
isSupported,
|
|
3189
|
+
error
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
function MicIcon({ size = 20, color = "currentColor" }) {
|
|
3193
|
+
return /* @__PURE__ */ jsx(
|
|
3194
|
+
"svg",
|
|
3195
|
+
{
|
|
3196
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
3197
|
+
width: size,
|
|
3198
|
+
height: size,
|
|
3199
|
+
viewBox: "0 0 24 24",
|
|
3200
|
+
fill: color,
|
|
3201
|
+
"aria-hidden": "true",
|
|
3202
|
+
children: /* @__PURE__ */ 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" })
|
|
3203
|
+
}
|
|
3204
|
+
);
|
|
3205
|
+
}
|
|
3091
3206
|
function ReplyPreview({ message, onCancel, className }) {
|
|
3092
3207
|
const preview = renderQuotedPreview(message);
|
|
3093
3208
|
return /* @__PURE__ */ jsxs("div", { className, style: styles7.container, children: [
|
|
@@ -3224,8 +3339,7 @@ function CloseIcon({ size = 18, color = "currentColor" }) {
|
|
|
3224
3339
|
}
|
|
3225
3340
|
function MessageInput({
|
|
3226
3341
|
onSendText,
|
|
3227
|
-
|
|
3228
|
-
// onSendAudio,
|
|
3342
|
+
onSendAudio,
|
|
3229
3343
|
onSendFile,
|
|
3230
3344
|
onTyping,
|
|
3231
3345
|
replyTo,
|
|
@@ -3242,6 +3356,15 @@ function MessageInput({
|
|
|
3242
3356
|
const typingTimeoutRef = useRef(null);
|
|
3243
3357
|
const isTypingRef = useRef(false);
|
|
3244
3358
|
const sendingRef = useRef(false);
|
|
3359
|
+
const {
|
|
3360
|
+
isRecording,
|
|
3361
|
+
duration,
|
|
3362
|
+
start: startRecording,
|
|
3363
|
+
stop: stopRecording,
|
|
3364
|
+
cancel: cancelRecording,
|
|
3365
|
+
isSupported: micSupported,
|
|
3366
|
+
error: micError
|
|
3367
|
+
} = useAudioRecorder();
|
|
3245
3368
|
const handleTyping = useCallback(() => {
|
|
3246
3369
|
if (!isTypingRef.current) {
|
|
3247
3370
|
isTypingRef.current = true;
|
|
@@ -3290,6 +3413,16 @@ function MessageInput({
|
|
|
3290
3413
|
},
|
|
3291
3414
|
[handleSendText]
|
|
3292
3415
|
);
|
|
3416
|
+
const handleAudioStop = useCallback(async () => {
|
|
3417
|
+
const result = await stopRecording();
|
|
3418
|
+
if (!result || !onSendAudio) return;
|
|
3419
|
+
setIsSending(true);
|
|
3420
|
+
try {
|
|
3421
|
+
await onSendAudio(result.blob, result.duration);
|
|
3422
|
+
} finally {
|
|
3423
|
+
setIsSending(false);
|
|
3424
|
+
}
|
|
3425
|
+
}, [stopRecording, onSendAudio]);
|
|
3293
3426
|
const handleFileSelect = useCallback(
|
|
3294
3427
|
async (e) => {
|
|
3295
3428
|
const file = e.target.files?.[0];
|
|
@@ -3330,7 +3463,18 @@ function MessageInput({
|
|
|
3330
3463
|
}
|
|
3331
3464
|
)
|
|
3332
3465
|
] }),
|
|
3333
|
-
/* @__PURE__ */
|
|
3466
|
+
micError && /* @__PURE__ */ jsx("div", { style: styles8.error, role: "alert", children: /* @__PURE__ */ jsx("span", { style: styles8.errorText, children: micError }) }),
|
|
3467
|
+
isRecording ? /* @__PURE__ */ jsxs("div", { style: styles8.recordingBar, children: [
|
|
3468
|
+
/* @__PURE__ */ jsxs("div", { style: styles8.recordingIndicator, children: [
|
|
3469
|
+
/* @__PURE__ */ jsx("span", { style: styles8.recordingDot }),
|
|
3470
|
+
"Recording ",
|
|
3471
|
+
formatDuration2(duration)
|
|
3472
|
+
] }),
|
|
3473
|
+
/* @__PURE__ */ jsxs("div", { style: styles8.recordingActions, children: [
|
|
3474
|
+
/* @__PURE__ */ jsx("button", { onClick: cancelRecording, style: styles8.cancelButton, type: "button", children: "Cancel" }),
|
|
3475
|
+
/* @__PURE__ */ jsx("button", { onClick: handleAudioStop, style: styles8.stopButton, type: "button", children: "Send" })
|
|
3476
|
+
] })
|
|
3477
|
+
] }) : /* @__PURE__ */ jsxs("div", { style: styles8.inputBar, children: [
|
|
3334
3478
|
/* @__PURE__ */ jsx(
|
|
3335
3479
|
"button",
|
|
3336
3480
|
{
|
|
@@ -3349,6 +3493,7 @@ function MessageInput({
|
|
|
3349
3493
|
ref: fileInputRef,
|
|
3350
3494
|
type: "file",
|
|
3351
3495
|
onChange: handleFileSelect,
|
|
3496
|
+
"aria-label": "Choose a file to upload",
|
|
3352
3497
|
style: { display: "none" },
|
|
3353
3498
|
accept: [
|
|
3354
3499
|
...SUPPORTED_IMAGE_TYPES,
|
|
@@ -3379,7 +3524,19 @@ function MessageInput({
|
|
|
3379
3524
|
style: styles8.textarea
|
|
3380
3525
|
}
|
|
3381
3526
|
),
|
|
3382
|
-
/* @__PURE__ */ jsx(
|
|
3527
|
+
onSendAudio && micSupported && !text.trim() && /* @__PURE__ */ jsx(
|
|
3528
|
+
"button",
|
|
3529
|
+
{
|
|
3530
|
+
onClick: startRecording,
|
|
3531
|
+
disabled: disabled || isSending,
|
|
3532
|
+
"aria-label": "Record voice message",
|
|
3533
|
+
title: "Record voice message",
|
|
3534
|
+
style: styles8.micButton,
|
|
3535
|
+
type: "button",
|
|
3536
|
+
children: /* @__PURE__ */ jsx(MicIcon, { size: 22, color: COLOR.neutral700 })
|
|
3537
|
+
}
|
|
3538
|
+
),
|
|
3539
|
+
text.trim() && /* @__PURE__ */ jsx(
|
|
3383
3540
|
"button",
|
|
3384
3541
|
{
|
|
3385
3542
|
onClick: handleSendText,
|
|
@@ -3397,6 +3554,11 @@ function MessageInput({
|
|
|
3397
3554
|
] })
|
|
3398
3555
|
] });
|
|
3399
3556
|
}
|
|
3557
|
+
function formatDuration2(seconds) {
|
|
3558
|
+
const m = Math.floor(seconds / 60);
|
|
3559
|
+
const s = seconds % 60;
|
|
3560
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
3561
|
+
}
|
|
3400
3562
|
var styles8 = {
|
|
3401
3563
|
container: {
|
|
3402
3564
|
borderTop: `1px solid ${COLOR.neutral200}`,
|
|
@@ -3473,6 +3635,23 @@ var styles8 = {
|
|
|
3473
3635
|
color: COLOR.neutral700,
|
|
3474
3636
|
transition: "background-color 120ms ease, color 120ms ease, border-color 120ms ease"
|
|
3475
3637
|
},
|
|
3638
|
+
// Same 48-square chrome as attach. Sits between the textarea and send
|
|
3639
|
+
// button when there's no draft text — replaces send while idle so the
|
|
3640
|
+
// input row stays one row tall.
|
|
3641
|
+
micButton: {
|
|
3642
|
+
width: "48px",
|
|
3643
|
+
height: "48px",
|
|
3644
|
+
display: "flex",
|
|
3645
|
+
alignItems: "center",
|
|
3646
|
+
justifyContent: "center",
|
|
3647
|
+
backgroundColor: COLOR.neutral100,
|
|
3648
|
+
border: `1.5px solid ${COLOR.neutral200}`,
|
|
3649
|
+
borderRadius: "14px",
|
|
3650
|
+
cursor: "pointer",
|
|
3651
|
+
flexShrink: 0,
|
|
3652
|
+
color: COLOR.neutral700,
|
|
3653
|
+
transition: "background-color 120ms ease, color 120ms ease, border-color 120ms ease"
|
|
3654
|
+
},
|
|
3476
3655
|
textarea: {
|
|
3477
3656
|
flex: 1,
|
|
3478
3657
|
minHeight: "48px",
|
|
@@ -3505,7 +3684,53 @@ var styles8 = {
|
|
|
3505
3684
|
cursor: "pointer",
|
|
3506
3685
|
flexShrink: 0,
|
|
3507
3686
|
transition: "opacity 120ms ease, background-color 120ms ease"
|
|
3508
|
-
}
|
|
3687
|
+
},
|
|
3688
|
+
recordingBar: {
|
|
3689
|
+
display: "flex",
|
|
3690
|
+
justifyContent: "space-between",
|
|
3691
|
+
alignItems: "center",
|
|
3692
|
+
padding: `${SPACE.S3} ${SPACE.S4}`,
|
|
3693
|
+
backgroundColor: COLOR.dangerBg
|
|
3694
|
+
},
|
|
3695
|
+
recordingIndicator: {
|
|
3696
|
+
display: "flex",
|
|
3697
|
+
alignItems: "center",
|
|
3698
|
+
gap: SPACE.S2,
|
|
3699
|
+
fontSize: FONT_SIZE.sm,
|
|
3700
|
+
color: COLOR.danger,
|
|
3701
|
+
fontWeight: FONT_WEIGHT.medium
|
|
3702
|
+
},
|
|
3703
|
+
recordingDot: {
|
|
3704
|
+
width: SPACE.S2,
|
|
3705
|
+
height: SPACE.S2,
|
|
3706
|
+
borderRadius: RADIUS.full,
|
|
3707
|
+
backgroundColor: COLOR.danger,
|
|
3708
|
+
animation: "pulse 1.5s infinite"
|
|
3709
|
+
},
|
|
3710
|
+
recordingActions: {
|
|
3711
|
+
display: "flex",
|
|
3712
|
+
gap: SPACE.S2
|
|
3713
|
+
},
|
|
3714
|
+
cancelButton: {
|
|
3715
|
+
padding: `${SPACE.S1} ${SPACE.S4}`,
|
|
3716
|
+
fontSize: FONT_SIZE.sm,
|
|
3717
|
+
color: COLOR.neutral500,
|
|
3718
|
+
backgroundColor: COLOR.white,
|
|
3719
|
+
border: `1px solid ${COLOR.neutral300}`,
|
|
3720
|
+
borderRadius: RADIUS.md,
|
|
3721
|
+
cursor: "pointer"
|
|
3722
|
+
},
|
|
3723
|
+
stopButton: {
|
|
3724
|
+
padding: `${SPACE.S1} ${SPACE.S4}`,
|
|
3725
|
+
fontSize: FONT_SIZE.sm,
|
|
3726
|
+
fontWeight: FONT_WEIGHT.medium,
|
|
3727
|
+
color: COLOR.white,
|
|
3728
|
+
backgroundColor: COLOR.danger,
|
|
3729
|
+
border: "none",
|
|
3730
|
+
borderRadius: RADIUS.md,
|
|
3731
|
+
cursor: "pointer"
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3509
3734
|
var ROLE_AVATAR_BG = {
|
|
3510
3735
|
radiologist: "#4f46e5",
|
|
3511
3736
|
// indigo
|
|
@@ -4932,6 +5157,7 @@ function useInlineCollab({
|
|
|
4932
5157
|
}
|
|
4933
5158
|
try {
|
|
4934
5159
|
await socket.sendMessage(preview.conversationId, payload);
|
|
5160
|
+
setUnreadCount(0);
|
|
4935
5161
|
} catch (err) {
|
|
4936
5162
|
setError(err instanceof Error ? err.message : "Failed to send message");
|
|
4937
5163
|
config.onError?.({
|
|
@@ -5187,7 +5413,7 @@ function InlineAudioPlayer({
|
|
|
5187
5413
|
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "70%", backgroundColor: pal.audioFg } }),
|
|
5188
5414
|
/* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } })
|
|
5189
5415
|
] }),
|
|
5190
|
-
/* @__PURE__ */ jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children:
|
|
5416
|
+
/* @__PURE__ */ jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children: formatDuration3(displayTime) }),
|
|
5191
5417
|
/* @__PURE__ */ jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
|
|
5192
5418
|
] });
|
|
5193
5419
|
}
|
|
@@ -5203,7 +5429,7 @@ function renderMessagePreview(message) {
|
|
|
5203
5429
|
return message.body;
|
|
5204
5430
|
}
|
|
5205
5431
|
}
|
|
5206
|
-
function
|
|
5432
|
+
function formatDuration3(seconds) {
|
|
5207
5433
|
const safe = Math.max(0, Math.floor(seconds));
|
|
5208
5434
|
const m = Math.floor(safe / 60);
|
|
5209
5435
|
const s = safe % 60;
|
|
@@ -5263,8 +5489,6 @@ function InlineInputBar({
|
|
|
5263
5489
|
}
|
|
5264
5490
|
}
|
|
5265
5491
|
),
|
|
5266
|
-
unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
|
|
5267
|
-
isLive && /* @__PURE__ */ jsx("span", { style: styles14.liveDot, title: "Live updates active" }),
|
|
5268
5492
|
text.trim() && /* @__PURE__ */ jsx(
|
|
5269
5493
|
"button",
|
|
5270
5494
|
{
|
|
@@ -5276,6 +5500,8 @@ function InlineInputBar({
|
|
|
5276
5500
|
children: "\u27A4"
|
|
5277
5501
|
}
|
|
5278
5502
|
),
|
|
5503
|
+
unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
|
|
5504
|
+
isLive && /* @__PURE__ */ jsx("span", { style: styles14.liveDot, title: "Live updates active" }),
|
|
5279
5505
|
showExpand && onExpand && /* @__PURE__ */ jsx(
|
|
5280
5506
|
"button",
|
|
5281
5507
|
{
|
|
@@ -6359,107 +6585,6 @@ function useMessages({
|
|
|
6359
6585
|
reset
|
|
6360
6586
|
};
|
|
6361
6587
|
}
|
|
6362
|
-
function useAudioRecorder() {
|
|
6363
|
-
const [isRecording, setIsRecording] = useState(false);
|
|
6364
|
-
const [duration, setDuration] = useState(0);
|
|
6365
|
-
const [error, setError] = useState(null);
|
|
6366
|
-
const mediaRecorderRef = useRef(null);
|
|
6367
|
-
const chunksRef = useRef([]);
|
|
6368
|
-
const streamRef = useRef(null);
|
|
6369
|
-
const timerRef = useRef(null);
|
|
6370
|
-
const startTimeRef = useRef(0);
|
|
6371
|
-
const resolveRef = useRef(null);
|
|
6372
|
-
const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
|
|
6373
|
-
const cleanup = useCallback(() => {
|
|
6374
|
-
if (timerRef.current) {
|
|
6375
|
-
clearInterval(timerRef.current);
|
|
6376
|
-
timerRef.current = null;
|
|
6377
|
-
}
|
|
6378
|
-
if (streamRef.current) {
|
|
6379
|
-
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
6380
|
-
streamRef.current = null;
|
|
6381
|
-
}
|
|
6382
|
-
mediaRecorderRef.current = null;
|
|
6383
|
-
chunksRef.current = [];
|
|
6384
|
-
setIsRecording(false);
|
|
6385
|
-
setDuration(0);
|
|
6386
|
-
}, []);
|
|
6387
|
-
const start = useCallback(async () => {
|
|
6388
|
-
if (!isSupported) {
|
|
6389
|
-
setError("Audio recording is not supported in this browser");
|
|
6390
|
-
return;
|
|
6391
|
-
}
|
|
6392
|
-
try {
|
|
6393
|
-
setError(null);
|
|
6394
|
-
chunksRef.current = [];
|
|
6395
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
6396
|
-
streamRef.current = stream;
|
|
6397
|
-
const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
|
|
6398
|
-
const recorder = new MediaRecorder(stream, { mimeType });
|
|
6399
|
-
mediaRecorderRef.current = recorder;
|
|
6400
|
-
recorder.ondataavailable = (event) => {
|
|
6401
|
-
if (event.data.size > 0) {
|
|
6402
|
-
chunksRef.current.push(event.data);
|
|
6403
|
-
}
|
|
6404
|
-
};
|
|
6405
|
-
recorder.onerror = () => {
|
|
6406
|
-
setError("Recording failed");
|
|
6407
|
-
cleanup();
|
|
6408
|
-
resolveRef.current?.(null);
|
|
6409
|
-
resolveRef.current = null;
|
|
6410
|
-
};
|
|
6411
|
-
recorder.onstop = () => {
|
|
6412
|
-
const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
6413
|
-
const blob = new Blob(chunksRef.current, { type: mimeType });
|
|
6414
|
-
cleanup();
|
|
6415
|
-
resolveRef.current?.({ blob, duration: finalDuration });
|
|
6416
|
-
resolveRef.current = null;
|
|
6417
|
-
};
|
|
6418
|
-
recorder.start(250);
|
|
6419
|
-
startTimeRef.current = Date.now();
|
|
6420
|
-
setIsRecording(true);
|
|
6421
|
-
timerRef.current = setInterval(() => {
|
|
6422
|
-
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
6423
|
-
setDuration(elapsed);
|
|
6424
|
-
}, 1e3);
|
|
6425
|
-
} catch (err) {
|
|
6426
|
-
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
6427
|
-
setError("Microphone access denied. Please allow microphone permissions.");
|
|
6428
|
-
} else {
|
|
6429
|
-
setError("Failed to start recording");
|
|
6430
|
-
}
|
|
6431
|
-
cleanup();
|
|
6432
|
-
}
|
|
6433
|
-
}, [isSupported, cleanup]);
|
|
6434
|
-
const stop = useCallback(async () => {
|
|
6435
|
-
return new Promise((resolve) => {
|
|
6436
|
-
if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
|
|
6437
|
-
resolve(null);
|
|
6438
|
-
return;
|
|
6439
|
-
}
|
|
6440
|
-
resolveRef.current = resolve;
|
|
6441
|
-
mediaRecorderRef.current.stop();
|
|
6442
|
-
});
|
|
6443
|
-
}, []);
|
|
6444
|
-
const cancel = useCallback(() => {
|
|
6445
|
-
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
6446
|
-
mediaRecorderRef.current.onstop = null;
|
|
6447
|
-
mediaRecorderRef.current.stop();
|
|
6448
|
-
}
|
|
6449
|
-
cleanup();
|
|
6450
|
-
resolveRef.current?.(null);
|
|
6451
|
-
resolveRef.current = null;
|
|
6452
|
-
}, [cleanup]);
|
|
6453
|
-
return {
|
|
6454
|
-
isRecording,
|
|
6455
|
-
duration,
|
|
6456
|
-
start,
|
|
6457
|
-
stop,
|
|
6458
|
-
cancel,
|
|
6459
|
-
isSupported,
|
|
6460
|
-
error
|
|
6461
|
-
};
|
|
6462
|
-
}
|
|
6463
6588
|
function useUnreadCount() {
|
|
6464
6589
|
const { socket, totalUnread } = useCollab();
|
|
6465
6590
|
const [counts, setCounts] = useState({});
|