From 5871d9f8cfae2d3f6223d653bc58ea780297a7da Mon Sep 17 00:00:00 2001 From: Arkaprabha Chakraborty Date: Sat, 1 Nov 2025 08:19:43 +0530 Subject: [PATCH] recordinggggggggggggg... --- client/app/room/page.tsx | 175 +++++++++++++----- client/components/AnimatedAvatar.tsx | 3 +- client/components/LeftPanel.tsx | 49 ++++-- client/components/MediaPanel.tsx | 41 ++++- client/components/RecordingPopup.tsx | 254 +++++++++++++++++++++++++++ server/main.go | 10 ++ 6 files changed, 465 insertions(+), 67 deletions(-) create mode 100644 client/components/RecordingPopup.tsx diff --git a/client/app/room/page.tsx b/client/app/room/page.tsx index 13a8e02..3981fcd 100644 --- a/client/app/room/page.tsx +++ b/client/app/room/page.tsx @@ -22,10 +22,10 @@ import { RefreshCw, TriangleAlert, } from "lucide-react"; -import { Skeleton } from "@/components/ui/skeleton"; import { CommentsPanel } from "@/components/RightPanel"; import { CodeEditor, CodeEditorRef } from "@/components/Editor"; import { LeftPanel } from "@/components/LeftPanel"; +import RecordingPopup from "@/components/RecordingPopup"; import { getThemeById, getNextTheme, @@ -229,6 +229,8 @@ const Room = () => { const [isMobile, setIsMobile] = useState(false); const [fileSizeError, setFileSizeError] = useState(null); const [purgeError, setPurgeError] = useState(null); + const [uploadProgress, setUploadProgress] = useState>([]); + const [isRecordingOpen, setIsRecordingOpen] = useState(false); // Detect mobile screen size useEffect(() => { @@ -719,6 +721,14 @@ const Room = () => { const maxFileSize = 10 * 1024 * 1024; // 10MB in bytes const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8090"; + // Initialize progress for all files + const initialProgress = Array.from(files).map(file => ({ + fileName: file.name, + progress: 0, + status: 'uploading' as const + })); + setUploadProgress(initialProgress); + for (let i = 0; i < files.length; i++) { const file = files[i]; @@ -726,6 +736,9 @@ const Room = () => { if (file.size > maxFileSize) { const fileSizeInMB = (file.size / (1024 * 1024)).toFixed(2); setFileSizeError(`File "${file.name}" (${fileSizeInMB}MB) exceeds 10MB limit`); + setUploadProgress(prev => prev.map(p => + p.fileName === file.name ? { ...p, status: 'error' as const } : p + )); continue; // Skip this file and continue with others } @@ -736,27 +749,51 @@ const Room = () => { formData.append("roomCode", roomCode!); formData.append("uploadedBy", currentUser.name); - // Upload file to HTTP server - const response = await fetch(`${httpUrl}/upload`, { - method: "POST", - body: formData, + // Use XMLHttpRequest for progress tracking + await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + const progress = Math.round((e.loaded / e.total) * 100); + setUploadProgress(prev => prev.map(p => + p.fileName === file.name ? { ...p, progress } : p + )); + } + }); + + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + setUploadProgress(prev => prev.map(p => + p.fileName === file.name ? { ...p, progress: 100, status: 'completed' as const } : p + )); + resolve(); + } else { + reject(new Error(`Upload failed: ${xhr.statusText}`)); + } + }); + + xhr.addEventListener('error', () => { + reject(new Error('Upload failed')); + }); + + xhr.open('POST', `${httpUrl}/upload`); + xhr.send(formData); }); - if (!response.ok) { - throw new Error(`Upload failed: ${response.statusText}`); - } - - const mediaFile: MediaFile = await response.json(); - - // Don't add to local state here - the WebSocket broadcast will handle it - // This prevents duplicate entries when the server broadcasts the upload - - console.log("File uploaded successfully:", mediaFile); + console.log("File uploaded successfully:", file.name); } catch (error) { console.error("Error uploading file:", error); - // You could show a toast notification here + setUploadProgress(prev => prev.map(p => + p.fileName === file.name ? { ...p, status: 'error' as const } : p + )); } } + + // Clear progress after a delay + setTimeout(() => { + setUploadProgress([]); + }, 3000); }; const handleFileDelete = async (fileId: string) => { @@ -810,7 +847,7 @@ const Room = () => { }`} >
-
+
{ return to home
-
+
{ > {`switch to ${getThemeById(getNextTheme(currentThemeId)?.id)?.name}`} + setIsRecordingOpen(true)} + > + record + + } + contentClassName="py-1 px-2 w-auto text-xs border-foreground" + > + record audio + {/* Panel Controls for mobile and when panels are hidden due to width */} {(isMobile || !showSidePanels) && ( @@ -1034,6 +1084,49 @@ const Room = () => {
)} + {/* Upload Progress Overlay */} + {uploadProgress.length > 0 && ( +
+ + + + Uploading Files + + + + {uploadProgress.map((upload, index) => ( +
+
+ + {upload.fileName} + + + {upload.status === 'completed' ? '✓' : + upload.status === 'error' ? '✗' : + `${upload.progress}%`} + +
+
+
+
+
+ ))} + + +
+ )} + {/* Custom Popup for non-critical messages */} {popupMessage && (
@@ -1140,40 +1233,32 @@ const Room = () => { {/* Content Warning Modal */} -
+ + {/* Recording Popup */} + setIsRecordingOpen(false)} + onFileUpload={handleFileUpload} + /> +
); }; -const SkeletonMirror = () => { - return ( -
-
-
-
-
- -
- -
-
- -
-
- - -
-
-
-
-
-
- ); -}; +const LoadingOverlay = () => ( +
+ + +
+

Loading...

+
+
+
+); const RoomWrapper = () => ( - }> + }>
diff --git a/client/components/AnimatedAvatar.tsx b/client/components/AnimatedAvatar.tsx index 42eac32..991aad6 100644 --- a/client/components/AnimatedAvatar.tsx +++ b/client/components/AnimatedAvatar.tsx @@ -10,9 +10,10 @@ export const AnimatedAvatar: React.FC = ({ className }) =>
diff --git a/client/components/LeftPanel.tsx b/client/components/LeftPanel.tsx index f4f2f73..d041b24 100644 --- a/client/components/LeftPanel.tsx +++ b/client/components/LeftPanel.tsx @@ -6,7 +6,6 @@ import { MediaModal } from '@/components/MediaModal'; import { AnimatedAvatar } from '@/components/AnimatedAvatar'; import { Users, - Circle, Upload, File, ImageIcon, @@ -268,13 +267,40 @@ export const LeftPanel: React.FC = ({ if (playingAudio === fileId) setPlayingAudio(null); }; - const handleDownload = (file: MediaFile) => { - const link = document.createElement('a'); - link.href = getFileUrl(file); - link.download = file.name; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const handleDownload = async (file: MediaFile) => { + try { + // Fetch the file as a blob to force download + const response = await fetch(getFileUrl(file)); + const blob = await response.blob(); + + // Create object URL for the blob + const url = URL.createObjectURL(blob); + + // Create download link + const link = document.createElement('a'); + link.href = url; + link.download = file.name; + link.style.display = 'none'; + + // Append to body, click, and remove + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up object URL + URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading file:', error); + // Fallback to direct link + const link = document.createElement('a'); + link.href = getFileUrl(file); + link.download = file.name; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } }; return ( @@ -461,7 +487,7 @@ export const LeftPanel: React.FC = ({
) : ( activeUsers.map((user) => { - const { status, color } = getStatusIndicator(user); + const { status } = getStatusIndicator(user); const isCurrentUser = currentUser && user.id === currentUser.id; return ( @@ -479,11 +505,6 @@ export const LeftPanel: React.FC = ({ {user.name.charAt(0).toUpperCase()}
)} -

diff --git a/client/components/MediaPanel.tsx b/client/components/MediaPanel.tsx index 54b3d5a..2760c42 100644 --- a/client/components/MediaPanel.tsx +++ b/client/components/MediaPanel.tsx @@ -148,13 +148,40 @@ export const MediaPanel: React.FC = ({ if (playingVideo === fileId) setPlayingVideo(null); }; - const handleDownload = (file: MediaFile) => { - const link = document.createElement('a'); - link.href = file.url; - link.download = file.name; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const handleDownload = async (file: MediaFile) => { + try { + // Fetch the file as a blob to force download + const response = await fetch(file.url); + const blob = await response.blob(); + + // Create object URL for the blob + const url = URL.createObjectURL(blob); + + // Create download link + const link = document.createElement('a'); + link.href = url; + link.download = file.name; + link.style.display = 'none'; + + // Append to body, click, and remove + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up object URL + URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading file:', error); + // Fallback to direct link + const link = document.createElement('a'); + link.href = file.url; + link.download = file.name; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } }; if (!isVisible) { diff --git a/client/components/RecordingPopup.tsx b/client/components/RecordingPopup.tsx new file mode 100644 index 0000000..607fa08 --- /dev/null +++ b/client/components/RecordingPopup.tsx @@ -0,0 +1,254 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { Mic, Pause, X } from "lucide-react"; + +interface RecordingPopupProps { + isOpen: boolean; + onClose: () => void; + onFileUpload: (files: FileList) => Promise; +} + +const RecordingPopup: React.FC = ({ isOpen, onClose, onFileUpload }) => { + const [position, setPosition] = useState({ x: 100, y: 100 }); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [isRecording, setIsRecording] = useState(false); + const [recordingTime, setRecordingTime] = useState(0); + const [mediaRecorder, setMediaRecorder] = useState(null); + const [recordedChunks, setRecordedChunks] = useState([]); + const popupRef = useRef(null); + const intervalRef = useRef(null); + + const handleMouseDown = (e: React.MouseEvent) => { + setIsDragging(true); + setDragStart({ + x: e.clientX - position.x, + y: e.clientY - position.y, + }); + }; + + const handleTouchStart = (e: React.TouchEvent) => { + setIsDragging(true); + const touch = e.touches[0]; + setDragStart({ + x: touch.clientX - position.x, + y: touch.clientY - position.y, + }); + }; + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!isDragging) return; + + const newX = e.clientX - dragStart.x; + const newY = e.clientY - dragStart.y; + + // Keep popup within viewport bounds + const maxX = window.innerWidth - 128; // popup width (w-32 = 8rem = 128px) + const maxY = window.innerHeight - 120; // popup height (smaller now) + + setPosition({ + x: Math.max(0, Math.min(newX, maxX)), + y: Math.max(0, Math.min(newY, maxY)), + }); + }, [isDragging, dragStart]); + + const handleTouchMove = useCallback((e: TouchEvent) => { + if (!isDragging) return; + e.preventDefault(); // Prevent scrolling while dragging + + const touch = e.touches[0]; + const newX = touch.clientX - dragStart.x; + const newY = touch.clientY - dragStart.y; + + // Keep popup within viewport bounds + const maxX = window.innerWidth - 128; // popup width (w-32 = 8rem = 128px) + const maxY = window.innerHeight - 120; // popup height (smaller now) + + setPosition({ + x: Math.max(0, Math.min(newX, maxX)), + y: Math.max(0, Math.min(newY, maxY)), + }); + }, [isDragging, dragStart]); + + const handleMouseUp = useCallback(() => { + setIsDragging(false); + }, []); + + const handleTouchEnd = useCallback(() => { + setIsDragging(false); + }, []); + + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + document.addEventListener('touchend', handleTouchEnd); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + }; + } + }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); + + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const recorder = new MediaRecorder(stream); + const chunks: Blob[] = []; + + recorder.ondataavailable = (e) => { + if (e.data.size > 0) { + chunks.push(e.data); + } + }; + + recorder.onstop = () => { + setRecordedChunks(chunks); + stream.getTracks().forEach(track => track.stop()); + }; + + setMediaRecorder(recorder); + setRecordedChunks([]); + recorder.start(); + setIsRecording(true); + setRecordingTime(0); + + intervalRef.current = setInterval(() => { + setRecordingTime(prev => prev + 1); + }, 1000); + } catch (error) { + console.error('Error starting recording:', error); + } + }; + + const stopRecording = useCallback(() => { + if (mediaRecorder && mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + } + setIsRecording(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }, [mediaRecorder]); + + const downloadRecording = () => { + if (recordedChunks.length > 0) { + const blob = new Blob(recordedChunks, { type: 'audio/webm' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.webm`; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + // Auto-upload recording when it stops + useEffect(() => { + if (recordedChunks.length > 0 && !isRecording) { + const blob = new Blob(recordedChunks, { type: 'audio/webm' }); + const fileName = `recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.webm`; + const file = new File([blob], fileName, { type: 'audio/webm' }); + + // Create a proper FileList + const dt = new DataTransfer(); + dt.items.add(file); + const fileList = dt.files; + + // Upload the recording + onFileUpload(fileList); + + // Clear recorded chunks to prevent re-upload + setRecordedChunks([]); + } + }, [recordedChunks, isRecording, onFileUpload]); + + // Clean up recording when popup is closed + useEffect(() => { + if (!isOpen && isRecording) { + stopRecording(); + } + }, [isOpen, isRecording, stopRecording]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + if (!isOpen) return null; + + return ( +

+
+ + +
+ {formatTime(recordingTime)} +
+ + +
+ + {recordedChunks.length > 0 && ( +
+ +
+ )} +
+ ); +}; + +export default RecordingPopup; \ No newline at end of file diff --git a/server/main.go b/server/main.go index 42d2cfe..0434240 100644 --- a/server/main.go +++ b/server/main.go @@ -409,6 +409,16 @@ func handleFileServe(w http.ResponseWriter, r *http.Request) { return } + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + http.Error(w, "File not found", http.StatusNotFound) + return + } + + // Set headers to force download + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + w.Header().Set("Content-Type", "application/octet-stream") + // Serve the file http.ServeFile(w, r, filePath) }