@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 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
- onSendAudio: (blob: Blob, duration: number) => Promise<void>;
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, onSendAudio, onSendFile, onTyping, replyTo, onCancelReply, disabled, placeholder, className, }: MessageInputProps): react_jsx_runtime.JSX.Element;
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
- onSendAudio: (blob: Blob, duration: number) => Promise<void>;
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, onSendAudio, onSendFile, onTyping, replyTo, onCancelReply, disabled, placeholder, className, }: MessageInputProps): react_jsx_runtime.JSX.Element;
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
- (fileError || micError) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles8.error, children: fileError || micError }),
2231
- isRecording ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.recordingBar, children: [
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: formatDuration3(displayTime) }),
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 formatDuration3(seconds) {
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: micError ?? 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({});