diff --git a/apps/matrix/apps/web/src/lib/components/chat/Message.svelte b/apps/matrix/apps/web/src/lib/components/chat/Message.svelte index aab60ca5a..db4261412 100644 --- a/apps/matrix/apps/web/src/lib/components/chat/Message.svelte +++ b/apps/matrix/apps/web/src/lib/components/chat/Message.svelte @@ -11,6 +11,7 @@ DownloadSimple, File as FileIcon, Play, + Pause, Image as ImageIcon, Lock, Warning, @@ -43,6 +44,50 @@ let imageLoading = $state(true); let imageError = $state(false); + // Audio player state + let audioElement: HTMLAudioElement | null = $state(null); + let isPlaying = $state(false); + let audioProgress = $state(0); + let audioDuration = $state(0); + + function toggleAudio() { + if (!audioElement) return; + if (isPlaying) { + audioElement.pause(); + } else { + audioElement.play(); + } + } + + function handleAudioTimeUpdate() { + if (!audioElement) return; + audioProgress = audioElement.currentTime; + } + + function handleAudioLoadedMetadata() { + if (!audioElement) return; + audioDuration = audioElement.duration; + } + + function handleAudioEnded() { + isPlaying = false; + audioProgress = 0; + } + + function seekAudio(e: MouseEvent) { + if (!audioElement || !audioDuration) return; + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + audioElement.currentTime = percent * audioDuration; + } + + function formatAudioTime(seconds: number): string { + if (!seconds || isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + } + let formattedTime = $derived(format(message.timestamp, 'HH:mm')); let formattedDate = $derived(() => { @@ -201,8 +246,72 @@ {/if} - {:else if message.type === 'm.file' || message.type === 'm.audio'} - + {:else if message.type === 'm.audio'} + +
+ {:else if message.type === 'm.file'} + | null = null; + // Set message content when editing $effect(() => { if (editMessage) { @@ -135,6 +144,95 @@ console.error('Failed to upload file'); } } + + // Voice recording functions + async function startRecording() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); + audioChunks = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunks.push(event.data); + } + }; + + mediaRecorder.onstop = async () => { + // Stop all tracks + stream.getTracks().forEach((track) => track.stop()); + + // Create blob and send + const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); + await sendVoiceMessage(audioBlob); + }; + + mediaRecorder.start(100); // Collect data every 100ms + isRecording = true; + recordingDuration = 0; + + // Start duration counter + recordingInterval = setInterval(() => { + recordingDuration++; + }, 1000); + } catch (err) { + console.error('Failed to start recording:', err); + } + } + + function stopRecording() { + if (mediaRecorder && isRecording) { + mediaRecorder.stop(); + isRecording = false; + + if (recordingInterval) { + clearInterval(recordingInterval); + recordingInterval = null; + } + } + } + + function cancelRecording() { + if (mediaRecorder && isRecording) { + // Stop without sending + mediaRecorder.ondataavailable = null; + mediaRecorder.onstop = () => { + // Just clean up, don't send + }; + mediaRecorder.stop(); + isRecording = false; + + if (recordingInterval) { + clearInterval(recordingInterval); + recordingInterval = null; + } + } + } + + async function sendVoiceMessage(blob: Blob) { + uploading = true; + uploadProgress = 0; + + // Create a File from the Blob + const file = new File([blob], `voice-${Date.now()}.webm`, { type: 'audio/webm' }); + + const success = await matrixStore.sendFile(file, (progress) => { + uploadProgress = progress; + }); + + uploading = false; + uploadProgress = 0; + + if (!success) { + console.error('Failed to send voice message'); + } + } + + function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }Aufnahme läuft...
+