@natoe/colab 0.1.0 → 0.1.2

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useContext, useMemo } from 'react';
1
+ import React4, { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useContext, useMemo } from 'react';
2
2
  import { Socket, Presence } from 'phoenix';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -1755,6 +1755,82 @@ var styles5 = {
1755
1755
  color: "#d1d5db"
1756
1756
  }
1757
1757
  };
1758
+
1759
+ // src/core/dateLabels.ts
1760
+ var SHORT_MONTHS = [
1761
+ "Jan",
1762
+ "Feb",
1763
+ "Mar",
1764
+ "Apr",
1765
+ "May",
1766
+ "Jun",
1767
+ "Jul",
1768
+ "Aug",
1769
+ "Sep",
1770
+ "Oct",
1771
+ "Nov",
1772
+ "Dec"
1773
+ ];
1774
+ var LONG_WEEKDAYS = [
1775
+ "Sunday",
1776
+ "Monday",
1777
+ "Tuesday",
1778
+ "Wednesday",
1779
+ "Thursday",
1780
+ "Friday",
1781
+ "Saturday"
1782
+ ];
1783
+ var ONE_DAY_MS = 864e5;
1784
+ function parseSafe(iso) {
1785
+ const d = new Date(iso);
1786
+ return Number.isNaN(d.getTime()) ? null : d;
1787
+ }
1788
+ function startOfDay(d) {
1789
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
1790
+ }
1791
+ function daysBetween(a, b) {
1792
+ return Math.floor((startOfDay(a).getTime() - startOfDay(b).getTime()) / ONE_DAY_MS);
1793
+ }
1794
+ function formatFullDate(d) {
1795
+ return `${d.getDate()} ${SHORT_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
1796
+ }
1797
+ function formatLocaleTime(d) {
1798
+ try {
1799
+ return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
1800
+ } catch {
1801
+ const h = d.getHours();
1802
+ const m = d.getMinutes().toString().padStart(2, "0");
1803
+ const suffix = h >= 12 ? "PM" : "AM";
1804
+ const h12 = (h + 11) % 12 + 1;
1805
+ return `${h12}:${m} ${suffix}`;
1806
+ }
1807
+ }
1808
+ function formatInboxTimestamp(iso) {
1809
+ if (!iso) return "";
1810
+ const d = parseSafe(iso);
1811
+ if (!d) return "";
1812
+ const diffDays = daysBetween(/* @__PURE__ */ new Date(), d);
1813
+ if (diffDays === 0) return formatLocaleTime(d);
1814
+ if (diffDays === 1) return "Yesterday";
1815
+ if (diffDays > 1 && diffDays < 7) return LONG_WEEKDAYS[d.getDay()];
1816
+ return formatFullDate(d);
1817
+ }
1818
+ function formatDayDivider(iso) {
1819
+ if (!iso) return "";
1820
+ const d = parseSafe(iso);
1821
+ if (!d) return "";
1822
+ const diffDays = daysBetween(/* @__PURE__ */ new Date(), d);
1823
+ if (diffDays === 0) return "Today";
1824
+ if (diffDays === 1) return "Yesterday";
1825
+ if (diffDays > 1 && diffDays < 7) return LONG_WEEKDAYS[d.getDay()];
1826
+ return formatFullDate(d);
1827
+ }
1828
+ function dayBucketKey(iso) {
1829
+ if (!iso) return "";
1830
+ const d = parseSafe(iso);
1831
+ if (!d) return "";
1832
+ return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
1833
+ }
1758
1834
  var MessageList = forwardRef(function MessageList2({
1759
1835
  messages,
1760
1836
  currentUserId,
@@ -1840,22 +1916,30 @@ var MessageList = forwardRef(function MessageList2({
1840
1916
  /* @__PURE__ */ jsx("p", { style: styles6.emptyTitle, children: "No messages yet" }),
1841
1917
  /* @__PURE__ */ jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation about this study" })
1842
1918
  ] }),
1843
- messages.map((message) => /* @__PURE__ */ jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsx(
1844
- MessageBubble,
1845
- {
1846
- message,
1847
- isOwn: message.senderId === currentUserId,
1848
- participants,
1849
- currentUserId,
1850
- showSeenBy,
1851
- onDeepLinkClick,
1852
- onReply,
1853
- onPin,
1854
- onUnpin,
1855
- onCopy,
1856
- pinDisabled
1857
- }
1858
- ) }, message.id)),
1919
+ messages.map((message, index) => {
1920
+ const currentBucket = dayBucketKey(message.insertedAt);
1921
+ const prevBucket = index === 0 ? null : dayBucketKey(messages[index - 1].insertedAt);
1922
+ const showDivider = currentBucket && currentBucket !== prevBucket;
1923
+ return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
1924
+ showDivider && /* @__PURE__ */ jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
1925
+ /* @__PURE__ */ jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsx(
1926
+ MessageBubble,
1927
+ {
1928
+ message,
1929
+ isOwn: message.senderId === currentUserId,
1930
+ participants,
1931
+ currentUserId,
1932
+ showSeenBy,
1933
+ onDeepLinkClick,
1934
+ onReply,
1935
+ onPin,
1936
+ onUnpin,
1937
+ onCopy,
1938
+ pinDisabled
1939
+ }
1940
+ ) })
1941
+ ] }, message.id);
1942
+ }),
1859
1943
  typingUsers.length > 0 && /* @__PURE__ */ jsxs("div", { style: styles6.typingIndicator, children: [
1860
1944
  typingUsers.map((t) => t.userName ?? "Someone").join(", "),
1861
1945
  typingUsers.length === 1 ? " is " : " are ",
@@ -1917,109 +2001,24 @@ var styles6 = {
1917
2001
  fontSize: "12px",
1918
2002
  color: "#9ca3af",
1919
2003
  fontStyle: "italic"
2004
+ },
2005
+ dayDivider: {
2006
+ display: "flex",
2007
+ justifyContent: "center",
2008
+ margin: "12px 0 8px"
2009
+ },
2010
+ dayDividerPill: {
2011
+ display: "inline-block",
2012
+ padding: "4px 12px",
2013
+ fontSize: "12px",
2014
+ fontWeight: 500,
2015
+ color: "#475569",
2016
+ backgroundColor: "#f1f5f9",
2017
+ border: "1px solid #e2e8f0",
2018
+ borderRadius: "12px",
2019
+ letterSpacing: "0.2px"
1920
2020
  }
1921
2021
  };
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
2022
  function ReplyPreview({ message, onCancel, className }) {
2024
2023
  const preview = renderQuotedPreview(message);
2025
2024
  return /* @__PURE__ */ jsxs("div", { className, style: styles7.container, children: [
@@ -2112,23 +2111,10 @@ var styles7 = {
2112
2111
  flexShrink: 0
2113
2112
  }
2114
2113
  };
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
2114
  function MessageInput({
2130
2115
  onSendText,
2131
- onSendAudio,
2116
+ // MIC DISABLED — onSendAudio still accepted but unused.
2117
+ // onSendAudio,
2132
2118
  onSendFile,
2133
2119
  onTyping,
2134
2120
  replyTo,
@@ -2144,7 +2130,6 @@ function MessageInput({
2144
2130
  const typingTimeoutRef = useRef(null);
2145
2131
  const isTypingRef = useRef(false);
2146
2132
  const sendingRef = useRef(false);
2147
- const { isRecording, duration, start: startRecording, stop: stopRecording, cancel: cancelRecording, isSupported: micSupported, error: micError } = useAudioRecorder();
2148
2133
  const handleTyping = useCallback(() => {
2149
2134
  if (!isTypingRef.current) {
2150
2135
  isTypingRef.current = true;
@@ -2193,17 +2178,6 @@ function MessageInput({
2193
2178
  },
2194
2179
  [handleSendText]
2195
2180
  );
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
2181
  const handleFileSelect = useCallback(
2208
2182
  async (e) => {
2209
2183
  const file = e.target.files?.[0];
@@ -2225,29 +2199,8 @@ function MessageInput({
2225
2199
  );
2226
2200
  return /* @__PURE__ */ jsxs("div", { className, style: styles8.container, children: [
2227
2201
  replyTo && onCancelReply && /* @__PURE__ */ jsx(ReplyPreview, { message: replyTo, onCancel: onCancelReply }),
2228
- (fileError || micError) && /* @__PURE__ */ jsx("div", { style: styles8.error, children: fileError || micError }),
2229
- isRecording ? /* @__PURE__ */ jsxs("div", { style: styles8.recordingBar, children: [
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
- ),
2202
+ fileError && /* @__PURE__ */ jsx("div", { style: styles8.error, children: fileError }),
2203
+ /* @__PURE__ */ jsxs("div", { style: styles8.inputBar, children: [
2251
2204
  /* @__PURE__ */ jsx(
2252
2205
  "button",
2253
2206
  {
@@ -2300,11 +2253,6 @@ function MessageInput({
2300
2253
  ] })
2301
2254
  ] });
2302
2255
  }
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
2256
  var styles8 = {
2309
2257
  container: {
2310
2258
  borderTop: "1px solid #e5e7eb",
@@ -2360,53 +2308,7 @@ var styles8 = {
2360
2308
  borderRadius: "50%",
2361
2309
  cursor: "pointer",
2362
2310
  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
- };
2311
+ }};
2410
2312
  function ParticipantsList({
2411
2313
  participants,
2412
2314
  currentUserId,
@@ -3708,7 +3610,7 @@ function InlineAudioPlayer({ url, duration }) {
3708
3610
  /* @__PURE__ */ jsx("span", { style: { ...styles13.waveBar, height: "70%" } }),
3709
3611
  /* @__PURE__ */ jsx("span", { style: { ...styles13.waveBar, height: "40%" } })
3710
3612
  ] }),
3711
- /* @__PURE__ */ jsx("span", { style: styles13.audioDuration, children: formatDuration3(displayTime) }),
3613
+ /* @__PURE__ */ jsx("span", { style: styles13.audioDuration, children: formatDuration2(displayTime) }),
3712
3614
  /* @__PURE__ */ jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
3713
3615
  ] });
3714
3616
  }
@@ -3724,7 +3626,7 @@ function renderMessagePreview(message) {
3724
3626
  return message.body;
3725
3627
  }
3726
3628
  }
3727
- function formatDuration3(seconds) {
3629
+ function formatDuration2(seconds) {
3728
3630
  const safe = Math.max(0, Math.floor(seconds));
3729
3631
  const m = Math.floor(safe / 60);
3730
3632
  const s = safe % 60;
@@ -3732,7 +3634,8 @@ function formatDuration3(seconds) {
3732
3634
  }
3733
3635
  function InlineInputBar({
3734
3636
  onSend,
3735
- onSendAudio,
3637
+ // MIC DISABLED — onSendAudio accepted but unused.
3638
+ // onSendAudio,
3736
3639
  placeholder,
3737
3640
  unreadCount,
3738
3641
  isLive,
@@ -3742,15 +3645,6 @@ function InlineInputBar({
3742
3645
  const [text, setText] = useState("");
3743
3646
  const [isSending, setIsSending] = useState(false);
3744
3647
  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
3648
  const handleSend = useCallback(async () => {
3755
3649
  const trimmed = text.trim();
3756
3650
  if (!trimmed || isSending) return;
@@ -3772,46 +3666,6 @@ function InlineInputBar({
3772
3666
  },
3773
3667
  [handleSend]
3774
3668
  );
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
3669
  return /* @__PURE__ */ jsxs("div", { style: styles13.inputBar, children: [
3816
3670
  /* @__PURE__ */ jsx(
3817
3671
  "input",
@@ -3821,24 +3675,13 @@ function InlineInputBar({
3821
3675
  value: text,
3822
3676
  onChange: (e) => setText(e.target.value),
3823
3677
  onKeyDown: handleKeyDown,
3824
- placeholder: micError ?? placeholder,
3678
+ placeholder,
3825
3679
  disabled: isSending,
3826
3680
  style: styles13.input
3827
3681
  }
3828
3682
  ),
3829
3683
  unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles13.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
3830
3684
  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
3685
  text.trim() && /* @__PURE__ */ jsx(
3843
3686
  "button",
3844
3687
  {
@@ -4016,19 +3859,6 @@ var styles13 = {
4016
3859
  borderRadius: "50%",
4017
3860
  backgroundColor: "#22c55e"
4018
3861
  },
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
3862
  sendButton: {
4033
3863
  flexShrink: 0,
4034
3864
  width: "28px",
@@ -4056,54 +3886,7 @@ var styles13 = {
4056
3886
  border: "1px solid #e5e7eb",
4057
3887
  borderRadius: "50%",
4058
3888
  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
- };
3889
+ }};
4107
3890
  function useConversationList(options) {
4108
3891
  const enabled = options?.enabled ?? true;
4109
3892
  const { fetchConversationList, config, totalUnread: serverTotalUnread } = useCollab();
@@ -4218,7 +4001,7 @@ function ConversationListItem({
4218
4001
  ...styles14.time,
4219
4002
  ...hasUnread ? styles14.timeUnread : {}
4220
4003
  },
4221
- children: formatRelativeTime(item.lastActivityAt)
4004
+ children: formatInboxTimestamp(item.lastActivityAt)
4222
4005
  }
4223
4006
  )
4224
4007
  ] }),
@@ -4261,23 +4044,6 @@ function renderLastMessagePreview(message) {
4261
4044
  }
4262
4045
  }
4263
4046
  }
4264
- function formatRelativeTime(iso) {
4265
- try {
4266
- const date = new Date(iso);
4267
- const now = /* @__PURE__ */ new Date();
4268
- const diffMs = now.getTime() - date.getTime();
4269
- const diffMin = Math.floor(diffMs / 6e4);
4270
- const diffHr = Math.floor(diffMs / 36e5);
4271
- const diffDay = Math.floor(diffMs / 864e5);
4272
- if (diffMin < 1) return "now";
4273
- if (diffMin < 60) return `${diffMin}m`;
4274
- if (diffHr < 24) return `${diffHr}h`;
4275
- if (diffDay < 7) return `${diffDay}d`;
4276
- return date.toLocaleDateString([], { month: "short", day: "numeric" });
4277
- } catch {
4278
- return "";
4279
- }
4280
- }
4281
4047
  var styles14 = {
4282
4048
  container: {
4283
4049
  display: "flex",
@@ -4777,6 +4543,107 @@ function useMessages({
4777
4543
  reset
4778
4544
  };
4779
4545
  }
4546
+ function useAudioRecorder() {
4547
+ const [isRecording, setIsRecording] = useState(false);
4548
+ const [duration, setDuration] = useState(0);
4549
+ const [error, setError] = useState(null);
4550
+ const mediaRecorderRef = useRef(null);
4551
+ const chunksRef = useRef([]);
4552
+ const streamRef = useRef(null);
4553
+ const timerRef = useRef(null);
4554
+ const startTimeRef = useRef(0);
4555
+ const resolveRef = useRef(null);
4556
+ const isSupported = typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!window.MediaRecorder;
4557
+ const cleanup = useCallback(() => {
4558
+ if (timerRef.current) {
4559
+ clearInterval(timerRef.current);
4560
+ timerRef.current = null;
4561
+ }
4562
+ if (streamRef.current) {
4563
+ streamRef.current.getTracks().forEach((track) => track.stop());
4564
+ streamRef.current = null;
4565
+ }
4566
+ mediaRecorderRef.current = null;
4567
+ chunksRef.current = [];
4568
+ setIsRecording(false);
4569
+ setDuration(0);
4570
+ }, []);
4571
+ const start = useCallback(async () => {
4572
+ if (!isSupported) {
4573
+ setError("Audio recording is not supported in this browser");
4574
+ return;
4575
+ }
4576
+ try {
4577
+ setError(null);
4578
+ chunksRef.current = [];
4579
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
4580
+ streamRef.current = stream;
4581
+ const mimeType = MediaRecorder.isTypeSupported(AUDIO_MIME_TYPE) ? AUDIO_MIME_TYPE : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4";
4582
+ const recorder = new MediaRecorder(stream, { mimeType });
4583
+ mediaRecorderRef.current = recorder;
4584
+ recorder.ondataavailable = (event) => {
4585
+ if (event.data.size > 0) {
4586
+ chunksRef.current.push(event.data);
4587
+ }
4588
+ };
4589
+ recorder.onerror = () => {
4590
+ setError("Recording failed");
4591
+ cleanup();
4592
+ resolveRef.current?.(null);
4593
+ resolveRef.current = null;
4594
+ };
4595
+ recorder.onstop = () => {
4596
+ const finalDuration = Math.round((Date.now() - startTimeRef.current) / 1e3);
4597
+ const blob = new Blob(chunksRef.current, { type: mimeType });
4598
+ cleanup();
4599
+ resolveRef.current?.({ blob, duration: finalDuration });
4600
+ resolveRef.current = null;
4601
+ };
4602
+ recorder.start(250);
4603
+ startTimeRef.current = Date.now();
4604
+ setIsRecording(true);
4605
+ timerRef.current = setInterval(() => {
4606
+ const elapsed = Math.round((Date.now() - startTimeRef.current) / 1e3);
4607
+ setDuration(elapsed);
4608
+ }, 1e3);
4609
+ } catch (err) {
4610
+ if (err instanceof DOMException && err.name === "NotAllowedError") {
4611
+ setError("Microphone access denied. Please allow microphone permissions.");
4612
+ } else {
4613
+ setError("Failed to start recording");
4614
+ }
4615
+ cleanup();
4616
+ }
4617
+ }, [isSupported, cleanup]);
4618
+ const stop = useCallback(async () => {
4619
+ return new Promise((resolve) => {
4620
+ if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
4621
+ resolve(null);
4622
+ return;
4623
+ }
4624
+ resolveRef.current = resolve;
4625
+ mediaRecorderRef.current.stop();
4626
+ });
4627
+ }, []);
4628
+ const cancel = useCallback(() => {
4629
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
4630
+ mediaRecorderRef.current.onstop = null;
4631
+ mediaRecorderRef.current.stop();
4632
+ }
4633
+ cleanup();
4634
+ resolveRef.current?.(null);
4635
+ resolveRef.current = null;
4636
+ }, [cleanup]);
4637
+ return {
4638
+ isRecording,
4639
+ duration,
4640
+ start,
4641
+ stop,
4642
+ cancel,
4643
+ isSupported,
4644
+ error
4645
+ };
4646
+ }
4780
4647
  function useUnreadCount() {
4781
4648
  const { socket, totalUnread } = useCollab();
4782
4649
  const [counts, setCounts] = useState({});