@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.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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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({});
|