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