@natoe/colab 0.1.0 → 0.1.1
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 +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +112 -328
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -328
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1919,107 +1919,6 @@ var styles6 = {
|
|
|
1919
1919
|
fontStyle: "italic"
|
|
1920
1920
|
}
|
|
1921
1921
|
};
|
|
1922
|
-
function useAudioRecorder() {
|
|
1923
|
-
const [isRecording, setIsRecording] = useState(false);
|
|
1924
|
-
const [duration, setDuration] = useState(0);
|
|
1925
|
-
const [error, setError] = useState(null);
|
|
1926
|
-
const mediaRecorderRef = useRef(null);
|
|
1927
|
-
const chunksRef = useRef([]);
|
|
1928
|
-
const streamRef = useRef(null);
|
|
1929
|
-
const timerRef = useRef(null);
|
|
1930
|
-
const startTimeRef = useRef(0);
|
|
1931
|
-
const resolveRef = useRef(null);
|
|
1932
|
-
const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
|
|
1933
|
-
const cleanup = useCallback(() => {
|
|
1934
|
-
if (timerRef.current) {
|
|
1935
|
-
clearInterval(timerRef.current);
|
|
1936
|
-
timerRef.current = null;
|
|
1937
|
-
}
|
|
1938
|
-
if (streamRef.current) {
|
|
1939
|
-
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
1940
|
-
streamRef.current = null;
|
|
1941
|
-
}
|
|
1942
|
-
mediaRecorderRef.current = null;
|
|
1943
|
-
chunksRef.current = [];
|
|
1944
|
-
setIsRecording(false);
|
|
1945
|
-
setDuration(0);
|
|
1946
|
-
}, []);
|
|
1947
|
-
const start = useCallback(async () => {
|
|
1948
|
-
if (!isSupported) {
|
|
1949
|
-
setError("Audio recording is not supported in this browser");
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
try {
|
|
1953
|
-
setError(null);
|
|
1954
|
-
chunksRef.current = [];
|
|
1955
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1956
|
-
streamRef.current = stream;
|
|
1957
|
-
const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
|
|
1958
|
-
const recorder = new MediaRecorder(stream, { mimeType });
|
|
1959
|
-
mediaRecorderRef.current = recorder;
|
|
1960
|
-
recorder.ondataavailable = (event) => {
|
|
1961
|
-
if (event.data.size > 0) {
|
|
1962
|
-
chunksRef.current.push(event.data);
|
|
1963
|
-
}
|
|
1964
|
-
};
|
|
1965
|
-
recorder.onerror = () => {
|
|
1966
|
-
setError("Recording failed");
|
|
1967
|
-
cleanup();
|
|
1968
|
-
resolveRef.current?.(null);
|
|
1969
|
-
resolveRef.current = null;
|
|
1970
|
-
};
|
|
1971
|
-
recorder.onstop = () => {
|
|
1972
|
-
const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
1973
|
-
const blob = new Blob(chunksRef.current, { type: mimeType });
|
|
1974
|
-
cleanup();
|
|
1975
|
-
resolveRef.current?.({ blob, duration: finalDuration });
|
|
1976
|
-
resolveRef.current = null;
|
|
1977
|
-
};
|
|
1978
|
-
recorder.start(250);
|
|
1979
|
-
startTimeRef.current = Date.now();
|
|
1980
|
-
setIsRecording(true);
|
|
1981
|
-
timerRef.current = setInterval(() => {
|
|
1982
|
-
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
1983
|
-
setDuration(elapsed);
|
|
1984
|
-
}, 1e3);
|
|
1985
|
-
} catch (err) {
|
|
1986
|
-
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
1987
|
-
setError("Microphone access denied. Please allow microphone permissions.");
|
|
1988
|
-
} else {
|
|
1989
|
-
setError("Failed to start recording");
|
|
1990
|
-
}
|
|
1991
|
-
cleanup();
|
|
1992
|
-
}
|
|
1993
|
-
}, [isSupported, cleanup]);
|
|
1994
|
-
const stop = useCallback(async () => {
|
|
1995
|
-
return new Promise((resolve) => {
|
|
1996
|
-
if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
|
|
1997
|
-
resolve(null);
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
resolveRef.current = resolve;
|
|
2001
|
-
mediaRecorderRef.current.stop();
|
|
2002
|
-
});
|
|
2003
|
-
}, []);
|
|
2004
|
-
const cancel = useCallback(() => {
|
|
2005
|
-
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
2006
|
-
mediaRecorderRef.current.onstop = null;
|
|
2007
|
-
mediaRecorderRef.current.stop();
|
|
2008
|
-
}
|
|
2009
|
-
cleanup();
|
|
2010
|
-
resolveRef.current?.(null);
|
|
2011
|
-
resolveRef.current = null;
|
|
2012
|
-
}, [cleanup]);
|
|
2013
|
-
return {
|
|
2014
|
-
isRecording,
|
|
2015
|
-
duration,
|
|
2016
|
-
start,
|
|
2017
|
-
stop,
|
|
2018
|
-
cancel,
|
|
2019
|
-
isSupported,
|
|
2020
|
-
error
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
1922
|
function ReplyPreview({ message, onCancel, className }) {
|
|
2024
1923
|
const preview = renderQuotedPreview(message);
|
|
2025
1924
|
return /* @__PURE__ */ jsxs("div", { className, style: styles7.container, children: [
|
|
@@ -2112,23 +2011,10 @@ var styles7 = {
|
|
|
2112
2011
|
flexShrink: 0
|
|
2113
2012
|
}
|
|
2114
2013
|
};
|
|
2115
|
-
function MicIcon({ size = 20, color = "currentColor" }) {
|
|
2116
|
-
return /* @__PURE__ */ jsx(
|
|
2117
|
-
"svg",
|
|
2118
|
-
{
|
|
2119
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
2120
|
-
width: size,
|
|
2121
|
-
height: size,
|
|
2122
|
-
viewBox: "0 0 24 24",
|
|
2123
|
-
fill: color,
|
|
2124
|
-
"aria-hidden": "true",
|
|
2125
|
-
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" })
|
|
2126
|
-
}
|
|
2127
|
-
);
|
|
2128
|
-
}
|
|
2129
2014
|
function MessageInput({
|
|
2130
2015
|
onSendText,
|
|
2131
|
-
onSendAudio
|
|
2016
|
+
// MIC DISABLED — onSendAudio still accepted but unused.
|
|
2017
|
+
// onSendAudio,
|
|
2132
2018
|
onSendFile,
|
|
2133
2019
|
onTyping,
|
|
2134
2020
|
replyTo,
|
|
@@ -2144,7 +2030,6 @@ function MessageInput({
|
|
|
2144
2030
|
const typingTimeoutRef = useRef(null);
|
|
2145
2031
|
const isTypingRef = useRef(false);
|
|
2146
2032
|
const sendingRef = useRef(false);
|
|
2147
|
-
const { isRecording, duration, start: startRecording, stop: stopRecording, cancel: cancelRecording, isSupported: micSupported, error: micError } = useAudioRecorder();
|
|
2148
2033
|
const handleTyping = useCallback(() => {
|
|
2149
2034
|
if (!isTypingRef.current) {
|
|
2150
2035
|
isTypingRef.current = true;
|
|
@@ -2193,17 +2078,6 @@ function MessageInput({
|
|
|
2193
2078
|
},
|
|
2194
2079
|
[handleSendText]
|
|
2195
2080
|
);
|
|
2196
|
-
const handleAudioStop = useCallback(async () => {
|
|
2197
|
-
const result = await stopRecording();
|
|
2198
|
-
if (result) {
|
|
2199
|
-
setIsSending(true);
|
|
2200
|
-
try {
|
|
2201
|
-
await onSendAudio(result.blob, result.duration);
|
|
2202
|
-
} finally {
|
|
2203
|
-
setIsSending(false);
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
}, [stopRecording, onSendAudio]);
|
|
2207
2081
|
const handleFileSelect = useCallback(
|
|
2208
2082
|
async (e) => {
|
|
2209
2083
|
const file = e.target.files?.[0];
|
|
@@ -2225,29 +2099,8 @@ function MessageInput({
|
|
|
2225
2099
|
);
|
|
2226
2100
|
return /* @__PURE__ */ jsxs("div", { className, style: styles8.container, children: [
|
|
2227
2101
|
replyTo && onCancelReply && /* @__PURE__ */ jsx(ReplyPreview, { message: replyTo, onCancel: onCancelReply }),
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
/* @__PURE__ */ jsxs("div", { style: styles8.recordingIndicator, children: [
|
|
2231
|
-
/* @__PURE__ */ jsx("span", { style: styles8.recordingDot }),
|
|
2232
|
-
"Recording ",
|
|
2233
|
-
formatDuration2(duration)
|
|
2234
|
-
] }),
|
|
2235
|
-
/* @__PURE__ */ jsxs("div", { style: styles8.recordingActions, children: [
|
|
2236
|
-
/* @__PURE__ */ jsx("button", { onClick: cancelRecording, style: styles8.cancelButton, type: "button", children: "Cancel" }),
|
|
2237
|
-
/* @__PURE__ */ jsx("button", { onClick: handleAudioStop, style: styles8.stopButton, type: "button", children: "Send" })
|
|
2238
|
-
] })
|
|
2239
|
-
] }) : /* @__PURE__ */ jsxs("div", { style: styles8.inputBar, children: [
|
|
2240
|
-
micSupported && /* @__PURE__ */ jsx(
|
|
2241
|
-
"button",
|
|
2242
|
-
{
|
|
2243
|
-
onClick: startRecording,
|
|
2244
|
-
disabled: disabled || isSending,
|
|
2245
|
-
style: styles8.iconButton,
|
|
2246
|
-
title: "Record voice message",
|
|
2247
|
-
type: "button",
|
|
2248
|
-
children: /* @__PURE__ */ jsx(MicIcon, { size: 20, color: "#374151" })
|
|
2249
|
-
}
|
|
2250
|
-
),
|
|
2102
|
+
fileError && /* @__PURE__ */ jsx("div", { style: styles8.error, children: fileError }),
|
|
2103
|
+
/* @__PURE__ */ jsxs("div", { style: styles8.inputBar, children: [
|
|
2251
2104
|
/* @__PURE__ */ jsx(
|
|
2252
2105
|
"button",
|
|
2253
2106
|
{
|
|
@@ -2300,11 +2153,6 @@ function MessageInput({
|
|
|
2300
2153
|
] })
|
|
2301
2154
|
] });
|
|
2302
2155
|
}
|
|
2303
|
-
function formatDuration2(seconds) {
|
|
2304
|
-
const m = Math.floor(seconds / 60);
|
|
2305
|
-
const s = seconds % 60;
|
|
2306
|
-
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
2307
|
-
}
|
|
2308
2156
|
var styles8 = {
|
|
2309
2157
|
container: {
|
|
2310
2158
|
borderTop: "1px solid #e5e7eb",
|
|
@@ -2360,53 +2208,7 @@ var styles8 = {
|
|
|
2360
2208
|
borderRadius: "50%",
|
|
2361
2209
|
cursor: "pointer",
|
|
2362
2210
|
flexShrink: 0
|
|
2363
|
-
}
|
|
2364
|
-
recordingBar: {
|
|
2365
|
-
display: "flex",
|
|
2366
|
-
justifyContent: "space-between",
|
|
2367
|
-
alignItems: "center",
|
|
2368
|
-
padding: "12px 16px",
|
|
2369
|
-
backgroundColor: "#fef2f2"
|
|
2370
|
-
},
|
|
2371
|
-
recordingIndicator: {
|
|
2372
|
-
display: "flex",
|
|
2373
|
-
alignItems: "center",
|
|
2374
|
-
gap: "8px",
|
|
2375
|
-
fontSize: "14px",
|
|
2376
|
-
color: "#dc2626",
|
|
2377
|
-
fontWeight: 500
|
|
2378
|
-
},
|
|
2379
|
-
recordingDot: {
|
|
2380
|
-
width: "8px",
|
|
2381
|
-
height: "8px",
|
|
2382
|
-
borderRadius: "50%",
|
|
2383
|
-
backgroundColor: "#dc2626",
|
|
2384
|
-
animation: "pulse 1.5s infinite"
|
|
2385
|
-
},
|
|
2386
|
-
recordingActions: {
|
|
2387
|
-
display: "flex",
|
|
2388
|
-
gap: "8px"
|
|
2389
|
-
},
|
|
2390
|
-
cancelButton: {
|
|
2391
|
-
padding: "6px 14px",
|
|
2392
|
-
fontSize: "13px",
|
|
2393
|
-
color: "#6b7280",
|
|
2394
|
-
backgroundColor: "#ffffff",
|
|
2395
|
-
border: "1px solid #d1d5db",
|
|
2396
|
-
borderRadius: "6px",
|
|
2397
|
-
cursor: "pointer"
|
|
2398
|
-
},
|
|
2399
|
-
stopButton: {
|
|
2400
|
-
padding: "6px 14px",
|
|
2401
|
-
fontSize: "13px",
|
|
2402
|
-
fontWeight: 500,
|
|
2403
|
-
color: "#ffffff",
|
|
2404
|
-
backgroundColor: "#dc2626",
|
|
2405
|
-
border: "none",
|
|
2406
|
-
borderRadius: "6px",
|
|
2407
|
-
cursor: "pointer"
|
|
2408
|
-
}
|
|
2409
|
-
};
|
|
2211
|
+
}};
|
|
2410
2212
|
function ParticipantsList({
|
|
2411
2213
|
participants,
|
|
2412
2214
|
currentUserId,
|
|
@@ -3708,7 +3510,7 @@ function InlineAudioPlayer({ url, duration }) {
|
|
|
3708
3510
|
/* @__PURE__ */ jsx("span", { style: { ...styles13.waveBar, height: "70%" } }),
|
|
3709
3511
|
/* @__PURE__ */ jsx("span", { style: { ...styles13.waveBar, height: "40%" } })
|
|
3710
3512
|
] }),
|
|
3711
|
-
/* @__PURE__ */ jsx("span", { style: styles13.audioDuration, children:
|
|
3513
|
+
/* @__PURE__ */ jsx("span", { style: styles13.audioDuration, children: formatDuration2(displayTime) }),
|
|
3712
3514
|
/* @__PURE__ */ jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
|
|
3713
3515
|
] });
|
|
3714
3516
|
}
|
|
@@ -3724,7 +3526,7 @@ function renderMessagePreview(message) {
|
|
|
3724
3526
|
return message.body;
|
|
3725
3527
|
}
|
|
3726
3528
|
}
|
|
3727
|
-
function
|
|
3529
|
+
function formatDuration2(seconds) {
|
|
3728
3530
|
const safe = Math.max(0, Math.floor(seconds));
|
|
3729
3531
|
const m = Math.floor(safe / 60);
|
|
3730
3532
|
const s = safe % 60;
|
|
@@ -3732,7 +3534,8 @@ function formatDuration3(seconds) {
|
|
|
3732
3534
|
}
|
|
3733
3535
|
function InlineInputBar({
|
|
3734
3536
|
onSend,
|
|
3735
|
-
onSendAudio
|
|
3537
|
+
// MIC DISABLED — onSendAudio accepted but unused.
|
|
3538
|
+
// onSendAudio,
|
|
3736
3539
|
placeholder,
|
|
3737
3540
|
unreadCount,
|
|
3738
3541
|
isLive,
|
|
@@ -3742,15 +3545,6 @@ function InlineInputBar({
|
|
|
3742
3545
|
const [text, setText] = useState("");
|
|
3743
3546
|
const [isSending, setIsSending] = useState(false);
|
|
3744
3547
|
const inputRef = useRef(null);
|
|
3745
|
-
const {
|
|
3746
|
-
isRecording,
|
|
3747
|
-
duration,
|
|
3748
|
-
start: startRecording,
|
|
3749
|
-
stop: stopRecording,
|
|
3750
|
-
cancel: cancelRecording,
|
|
3751
|
-
isSupported: micSupported,
|
|
3752
|
-
error: micError
|
|
3753
|
-
} = useAudioRecorder();
|
|
3754
3548
|
const handleSend = useCallback(async () => {
|
|
3755
3549
|
const trimmed = text.trim();
|
|
3756
3550
|
if (!trimmed || isSending) return;
|
|
@@ -3772,46 +3566,6 @@ function InlineInputBar({
|
|
|
3772
3566
|
},
|
|
3773
3567
|
[handleSend]
|
|
3774
3568
|
);
|
|
3775
|
-
const handleStopRecording = useCallback(async () => {
|
|
3776
|
-
const result = await stopRecording();
|
|
3777
|
-
if (result) {
|
|
3778
|
-
setIsSending(true);
|
|
3779
|
-
try {
|
|
3780
|
-
await onSendAudio(result.blob, result.duration);
|
|
3781
|
-
} finally {
|
|
3782
|
-
setIsSending(false);
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
}, [stopRecording, onSendAudio]);
|
|
3786
|
-
if (isRecording) {
|
|
3787
|
-
return /* @__PURE__ */ jsxs("div", { style: styles13.recordingBar, children: [
|
|
3788
|
-
/* @__PURE__ */ jsx("span", { style: styles13.recordingDot }),
|
|
3789
|
-
/* @__PURE__ */ jsxs("span", { style: styles13.recordingLabel, children: [
|
|
3790
|
-
"Recording ",
|
|
3791
|
-
formatDuration3(duration)
|
|
3792
|
-
] }),
|
|
3793
|
-
/* @__PURE__ */ jsx(
|
|
3794
|
-
"button",
|
|
3795
|
-
{
|
|
3796
|
-
onClick: cancelRecording,
|
|
3797
|
-
style: styles13.recordingCancel,
|
|
3798
|
-
title: "Cancel recording",
|
|
3799
|
-
type: "button",
|
|
3800
|
-
children: "Cancel"
|
|
3801
|
-
}
|
|
3802
|
-
),
|
|
3803
|
-
/* @__PURE__ */ jsx(
|
|
3804
|
-
"button",
|
|
3805
|
-
{
|
|
3806
|
-
onClick: handleStopRecording,
|
|
3807
|
-
style: styles13.recordingSend,
|
|
3808
|
-
title: "Send voice message",
|
|
3809
|
-
type: "button",
|
|
3810
|
-
children: "Send"
|
|
3811
|
-
}
|
|
3812
|
-
)
|
|
3813
|
-
] });
|
|
3814
|
-
}
|
|
3815
3569
|
return /* @__PURE__ */ jsxs("div", { style: styles13.inputBar, children: [
|
|
3816
3570
|
/* @__PURE__ */ jsx(
|
|
3817
3571
|
"input",
|
|
@@ -3821,24 +3575,13 @@ function InlineInputBar({
|
|
|
3821
3575
|
value: text,
|
|
3822
3576
|
onChange: (e) => setText(e.target.value),
|
|
3823
3577
|
onKeyDown: handleKeyDown,
|
|
3824
|
-
placeholder
|
|
3578
|
+
placeholder,
|
|
3825
3579
|
disabled: isSending,
|
|
3826
3580
|
style: styles13.input
|
|
3827
3581
|
}
|
|
3828
3582
|
),
|
|
3829
3583
|
unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles13.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
|
|
3830
3584
|
isLive && /* @__PURE__ */ jsx("span", { style: styles13.liveDot, title: "Live updates active" }),
|
|
3831
|
-
micSupported && !text.trim() && /* @__PURE__ */ jsx(
|
|
3832
|
-
"button",
|
|
3833
|
-
{
|
|
3834
|
-
onClick: startRecording,
|
|
3835
|
-
disabled: isSending,
|
|
3836
|
-
style: styles13.micButton,
|
|
3837
|
-
title: "Record voice message",
|
|
3838
|
-
type: "button",
|
|
3839
|
-
children: /* @__PURE__ */ jsx(MicIcon, { size: 16, color: "#374151" })
|
|
3840
|
-
}
|
|
3841
|
-
),
|
|
3842
3585
|
text.trim() && /* @__PURE__ */ jsx(
|
|
3843
3586
|
"button",
|
|
3844
3587
|
{
|
|
@@ -4016,19 +3759,6 @@ var styles13 = {
|
|
|
4016
3759
|
borderRadius: "50%",
|
|
4017
3760
|
backgroundColor: "#22c55e"
|
|
4018
3761
|
},
|
|
4019
|
-
micButton: {
|
|
4020
|
-
flexShrink: 0,
|
|
4021
|
-
width: "28px",
|
|
4022
|
-
height: "28px",
|
|
4023
|
-
display: "flex",
|
|
4024
|
-
alignItems: "center",
|
|
4025
|
-
justifyContent: "center",
|
|
4026
|
-
fontSize: "13px",
|
|
4027
|
-
backgroundColor: "#f3f4f6",
|
|
4028
|
-
border: "none",
|
|
4029
|
-
borderRadius: "50%",
|
|
4030
|
-
cursor: "pointer"
|
|
4031
|
-
},
|
|
4032
3762
|
sendButton: {
|
|
4033
3763
|
flexShrink: 0,
|
|
4034
3764
|
width: "28px",
|
|
@@ -4056,54 +3786,7 @@ var styles13 = {
|
|
|
4056
3786
|
border: "1px solid #e5e7eb",
|
|
4057
3787
|
borderRadius: "50%",
|
|
4058
3788
|
cursor: "pointer"
|
|
4059
|
-
}
|
|
4060
|
-
// ── Recording mode ──
|
|
4061
|
-
recordingBar: {
|
|
4062
|
-
display: "flex",
|
|
4063
|
-
alignItems: "center",
|
|
4064
|
-
gap: "8px",
|
|
4065
|
-
padding: "6px 10px",
|
|
4066
|
-
backgroundColor: "#fef2f2",
|
|
4067
|
-
border: "1px solid #fecaca",
|
|
4068
|
-
borderRadius: "16px"
|
|
4069
|
-
},
|
|
4070
|
-
recordingDot: {
|
|
4071
|
-
width: "8px",
|
|
4072
|
-
height: "8px",
|
|
4073
|
-
borderRadius: "50%",
|
|
4074
|
-
backgroundColor: "#dc2626",
|
|
4075
|
-
flexShrink: 0,
|
|
4076
|
-
animation: "pulse 1.5s infinite"
|
|
4077
|
-
},
|
|
4078
|
-
recordingLabel: {
|
|
4079
|
-
flex: 1,
|
|
4080
|
-
fontSize: "12px",
|
|
4081
|
-
fontWeight: 500,
|
|
4082
|
-
color: "#dc2626",
|
|
4083
|
-
fontVariantNumeric: "tabular-nums"
|
|
4084
|
-
},
|
|
4085
|
-
recordingCancel: {
|
|
4086
|
-
padding: "4px 10px",
|
|
4087
|
-
fontSize: "11px",
|
|
4088
|
-
color: "#6b7280",
|
|
4089
|
-
backgroundColor: "#ffffff",
|
|
4090
|
-
border: "1px solid #d1d5db",
|
|
4091
|
-
borderRadius: "12px",
|
|
4092
|
-
cursor: "pointer",
|
|
4093
|
-
flexShrink: 0
|
|
4094
|
-
},
|
|
4095
|
-
recordingSend: {
|
|
4096
|
-
padding: "4px 10px",
|
|
4097
|
-
fontSize: "11px",
|
|
4098
|
-
fontWeight: 500,
|
|
4099
|
-
color: "#ffffff",
|
|
4100
|
-
backgroundColor: "#dc2626",
|
|
4101
|
-
border: "none",
|
|
4102
|
-
borderRadius: "12px",
|
|
4103
|
-
cursor: "pointer",
|
|
4104
|
-
flexShrink: 0
|
|
4105
|
-
}
|
|
4106
|
-
};
|
|
3789
|
+
}};
|
|
4107
3790
|
function useConversationList(options) {
|
|
4108
3791
|
const enabled = options?.enabled ?? true;
|
|
4109
3792
|
const { fetchConversationList, config, totalUnread: serverTotalUnread } = useCollab();
|
|
@@ -4777,6 +4460,107 @@ function useMessages({
|
|
|
4777
4460
|
reset
|
|
4778
4461
|
};
|
|
4779
4462
|
}
|
|
4463
|
+
function useAudioRecorder() {
|
|
4464
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
4465
|
+
const [duration, setDuration] = useState(0);
|
|
4466
|
+
const [error, setError] = useState(null);
|
|
4467
|
+
const mediaRecorderRef = useRef(null);
|
|
4468
|
+
const chunksRef = useRef([]);
|
|
4469
|
+
const streamRef = useRef(null);
|
|
4470
|
+
const timerRef = useRef(null);
|
|
4471
|
+
const startTimeRef = useRef(0);
|
|
4472
|
+
const resolveRef = useRef(null);
|
|
4473
|
+
const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
|
|
4474
|
+
const cleanup = useCallback(() => {
|
|
4475
|
+
if (timerRef.current) {
|
|
4476
|
+
clearInterval(timerRef.current);
|
|
4477
|
+
timerRef.current = null;
|
|
4478
|
+
}
|
|
4479
|
+
if (streamRef.current) {
|
|
4480
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
4481
|
+
streamRef.current = null;
|
|
4482
|
+
}
|
|
4483
|
+
mediaRecorderRef.current = null;
|
|
4484
|
+
chunksRef.current = [];
|
|
4485
|
+
setIsRecording(false);
|
|
4486
|
+
setDuration(0);
|
|
4487
|
+
}, []);
|
|
4488
|
+
const start = useCallback(async () => {
|
|
4489
|
+
if (!isSupported) {
|
|
4490
|
+
setError("Audio recording is not supported in this browser");
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
try {
|
|
4494
|
+
setError(null);
|
|
4495
|
+
chunksRef.current = [];
|
|
4496
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
4497
|
+
streamRef.current = stream;
|
|
4498
|
+
const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
|
|
4499
|
+
const recorder = new MediaRecorder(stream, { mimeType });
|
|
4500
|
+
mediaRecorderRef.current = recorder;
|
|
4501
|
+
recorder.ondataavailable = (event) => {
|
|
4502
|
+
if (event.data.size > 0) {
|
|
4503
|
+
chunksRef.current.push(event.data);
|
|
4504
|
+
}
|
|
4505
|
+
};
|
|
4506
|
+
recorder.onerror = () => {
|
|
4507
|
+
setError("Recording failed");
|
|
4508
|
+
cleanup();
|
|
4509
|
+
resolveRef.current?.(null);
|
|
4510
|
+
resolveRef.current = null;
|
|
4511
|
+
};
|
|
4512
|
+
recorder.onstop = () => {
|
|
4513
|
+
const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
4514
|
+
const blob = new Blob(chunksRef.current, { type: mimeType });
|
|
4515
|
+
cleanup();
|
|
4516
|
+
resolveRef.current?.({ blob, duration: finalDuration });
|
|
4517
|
+
resolveRef.current = null;
|
|
4518
|
+
};
|
|
4519
|
+
recorder.start(250);
|
|
4520
|
+
startTimeRef.current = Date.now();
|
|
4521
|
+
setIsRecording(true);
|
|
4522
|
+
timerRef.current = setInterval(() => {
|
|
4523
|
+
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
4524
|
+
setDuration(elapsed);
|
|
4525
|
+
}, 1e3);
|
|
4526
|
+
} catch (err) {
|
|
4527
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
4528
|
+
setError("Microphone access denied. Please allow microphone permissions.");
|
|
4529
|
+
} else {
|
|
4530
|
+
setError("Failed to start recording");
|
|
4531
|
+
}
|
|
4532
|
+
cleanup();
|
|
4533
|
+
}
|
|
4534
|
+
}, [isSupported, cleanup]);
|
|
4535
|
+
const stop = useCallback(async () => {
|
|
4536
|
+
return new Promise((resolve) => {
|
|
4537
|
+
if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
|
|
4538
|
+
resolve(null);
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
resolveRef.current = resolve;
|
|
4542
|
+
mediaRecorderRef.current.stop();
|
|
4543
|
+
});
|
|
4544
|
+
}, []);
|
|
4545
|
+
const cancel = useCallback(() => {
|
|
4546
|
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
4547
|
+
mediaRecorderRef.current.onstop = null;
|
|
4548
|
+
mediaRecorderRef.current.stop();
|
|
4549
|
+
}
|
|
4550
|
+
cleanup();
|
|
4551
|
+
resolveRef.current?.(null);
|
|
4552
|
+
resolveRef.current = null;
|
|
4553
|
+
}, [cleanup]);
|
|
4554
|
+
return {
|
|
4555
|
+
isRecording,
|
|
4556
|
+
duration,
|
|
4557
|
+
start,
|
|
4558
|
+
stop,
|
|
4559
|
+
cancel,
|
|
4560
|
+
isSupported,
|
|
4561
|
+
error
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4780
4564
|
function useUnreadCount() {
|
|
4781
4565
|
const { socket, totalUnread } = useCollab();
|
|
4782
4566
|
const [counts, setCounts] = useState({});
|