@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.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +401 -530
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -363
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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) =>
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
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
|
-
|
|
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
|
-
),
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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({});
|