import { useEffect, useRef, useState } from "react"; import { attachFile, requestUploadUrl, sendMessage, uploadToPresignedUrl } from "../api/chats"; import { useAuthStore } from "../store/authStore"; import { useChatStore } from "../store/chatStore"; import { buildWsUrl } from "../utils/ws"; export function MessageComposer() { const activeChatId = useChatStore((s) => s.activeChatId); const prependMessage = useChatStore((s) => s.prependMessage); const accessToken = useAuthStore((s) => s.accessToken); const [text, setText] = useState(""); const wsRef = useRef(null); const recorderRef = useRef(null); const chunksRef = useRef([]); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [selectedType, setSelectedType] = useState<"file" | "image" | "video" | "audio">("file"); const [previewUrl, setPreviewUrl] = useState(null); const [isRecording, setIsRecording] = useState(false); useEffect(() => { return () => { if (previewUrl) { URL.revokeObjectURL(previewUrl); } }; }, [previewUrl]); function getWs(): WebSocket | null { if (!accessToken || !activeChatId) { return null; } if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { return wsRef.current; } const wsUrl = buildWsUrl(accessToken); wsRef.current = new WebSocket(wsUrl); return wsRef.current; } async function handleSend() { if (!activeChatId || !text.trim()) { return; } const message = await sendMessage(activeChatId, text.trim(), "text"); prependMessage(activeChatId, message); setText(""); const ws = getWs(); ws?.send(JSON.stringify({ event: "typing_stop", payload: { chat_id: activeChatId } })); } async function handleUpload(file: File, messageType: "file" | "image" | "video" | "audio" | "voice" = "file") { if (!activeChatId) { return; } setIsUploading(true); setUploadProgress(0); setUploadError(null); try { const upload = await requestUploadUrl(file); await uploadToPresignedUrl(upload.upload_url, upload.required_headers, file, setUploadProgress); const message = await sendMessage(activeChatId, upload.file_url, messageType); await attachFile(message.id, upload.file_url, file.type || "application/octet-stream", file.size); prependMessage(activeChatId, message); } catch { setUploadError("Upload failed. Please try again."); } finally { setIsUploading(false); } } function inferType(file: File): "file" | "image" | "video" | "audio" { if (file.type.startsWith("image/")) { return "image"; } if (file.type.startsWith("video/")) { return "video"; } if (file.type.startsWith("audio/")) { return "audio"; } return "file"; } async function startRecord() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const recorder = new MediaRecorder(stream); chunksRef.current = []; recorder.ondataavailable = (e) => chunksRef.current.push(e.data); recorder.onstop = async () => { const blob = new Blob(chunksRef.current, { type: "audio/webm" }); const file = new File([blob], `voice-${Date.now()}.webm`, { type: "audio/webm" }); setIsRecording(false); await handleUpload(file, "voice"); }; recorderRef.current = recorder; recorder.start(); setIsRecording(true); } catch { setUploadError("Microphone access denied."); } } function stopRecord() { recorderRef.current?.stop(); recorderRef.current = null; setIsRecording(false); } function selectFile(file: File) { setUploadError(null); setSelectedFile(file); const fileType = inferType(file); setSelectedType(fileType); if (previewUrl) { URL.revokeObjectURL(previewUrl); } if (fileType === "image" || fileType === "video") { setPreviewUrl(URL.createObjectURL(file)); } else { setPreviewUrl(null); } } async function sendSelectedFile() { if (!selectedFile) { return; } await handleUpload(selectedFile, selectedType); if (previewUrl) { URL.revokeObjectURL(previewUrl); } setSelectedFile(null); setPreviewUrl(null); setSelectedType("file"); setUploadProgress(0); } function cancelSelectedFile() { if (previewUrl) { URL.revokeObjectURL(previewUrl); } setSelectedFile(null); setPreviewUrl(null); setSelectedType("file"); setUploadProgress(0); setUploadError(null); } function formatBytes(size: number): string { if (size < 1024) { return `${size} B`; } if (size < 1024 * 1024) { return `${(size / 1024).toFixed(1)} KB`; } return `${(size / (1024 * 1024)).toFixed(1)} MB`; } return (
{ setText(e.target.value); if (activeChatId) { const ws = getWs(); ws?.send(JSON.stringify({ event: "typing_start", payload: { chat_id: activeChatId } })); } }} />
{selectedFile ? (
Ready to send
{selectedFile.name}
{formatBytes(selectedFile.size)}
{previewUrl && selectedType === "image" ? ( {selectedFile.name} ) : null} {previewUrl && selectedType === "video" ? (
); }