From 3bb4e523650e8bc660fb459829a6807e54dceff2 Mon Sep 17 00:00:00 2001 From: Arkaprabha Chakraborty Date: Fri, 31 Oct 2025 00:20:58 +0530 Subject: [PATCH] feat purge button, delete comments --- client/app/room/page.tsx | 138 ++++++++++++++++++++++------ client/components/CommentsPanel.tsx | 26 +++++- client/components/RightPanel.tsx | 26 +++++- server/main.go | 129 +++++++++++++++++++++++--- 4 files changed, 267 insertions(+), 52 deletions(-) diff --git a/client/app/room/page.tsx b/client/app/room/page.tsx index 9f30421..48bec02 100644 --- a/client/app/room/page.tsx +++ b/client/app/room/page.tsx @@ -208,6 +208,7 @@ const Room = () => { const [status, setStatus] = useState("Disconnected"); const [error, setError] = useState(""); const [isModalOpen, setIsModalOpen] = useState(false); + const [isPurgeModalOpen, setIsPurgeModalOpen] = useState(false); const [showDisconnectToast, setShowDisconnectToast] = useState(false); const [currentThemeId, setCurrentThemeId] = useState("one-dark-pro"); const [selectedLineStart, setSelectedLineStart] = useState(); @@ -656,11 +657,52 @@ const Room = () => { socketRef.current.send(JSON.stringify(message)); }; + const handleDeleteComment = (commentId: string) => { + if (!socketRef.current || !currentUser) return; + + const message: CommentMessage = { + type: "comment-delete", + code: roomCode!, + comment: { + id: commentId, + lineNumber: null, + author: "", + authorId: "", + content: "", + timestamp: new Date(), + }, + }; + + socketRef.current.send(JSON.stringify(message)); + }; + + const handlePurgeRoom = async () => { + if (!roomCode) return; + + try { + const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8090"; + const response = await fetch(`${httpUrl}/purge/${roomCode}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error(`Purge failed: ${response.statusText}`); + } + + router.push("/"); + } catch (error) { + console.error("Error purging room:", error); + showPopup("Failed to purge room", "warning"); + } + + setIsPurgeModalOpen(false); + }; + const handleFileUpload = async (files: FileList) => { if (!files || files.length === 0 || !currentUser) return; const maxFileSize = 10 * 1024 * 1024; // 10MB in bytes - const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8081"; + const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8090"; for (let i = 0; i < files.length; i++) { const file = files[i]; @@ -680,7 +722,7 @@ const Room = () => { formData.append("uploadedBy", currentUser.name); // Upload file to HTTP server - const response = await fetch(`${httpUrl}/upload`, { + const response = await fetch(`${httpUrl}/o/upload`, { method: "POST", body: formData, }); @@ -705,10 +747,10 @@ const Room = () => { const handleFileDelete = async (fileId: string) => { if (!roomCode) return; - const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8081"; + const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8090"; try { - const response = await fetch(`${httpUrl}/delete/${roomCode}/${fileId}`, { + const response = await fetch(`${httpUrl}/o/delete/${roomCode}/${fileId}`, { method: "DELETE", }); @@ -727,7 +769,7 @@ const Room = () => { const showPopup = (message: string, type: 'default' | 'warning' = 'default') => { setPopupMessage({text: message, type}); - setTimeout(() => setPopupMessage(null), 2000); + setTimeout(() => setPopupMessage(null), 3000); }; if (!isClient) return null; @@ -748,27 +790,11 @@ const Room = () => { >
- - - - - - copy room code: {roomCode} - - - + copy link to this page + + + + + + permanently delete this room and all its contents + + - + return to home @@ -814,7 +854,7 @@ const Room = () => { upload - + upload files @@ -832,7 +872,7 @@ const Room = () => { theme - + {getThemeById(currentThemeId)?.name || "Switch theme"} @@ -849,7 +889,7 @@ const Room = () => { media - + toggle users & media panel @@ -862,7 +902,7 @@ const Room = () => { notes - + toggle comments panel @@ -924,6 +964,7 @@ const Room = () => { onCommentSelect={handleCommentSelect} comments={comments} onAddComment={handleAddComment} + onDeleteComment={handleDeleteComment} currentUser={currentUser} /> @@ -961,6 +1002,47 @@ const Room = () => { onModalStateChange={setIsModalOpen} /> + {/* Purge Confirmation Modal */} + {isPurgeModalOpen && ( +
+ {/* Blurred overlay */} +
setIsPurgeModalOpen(false)} + /> + {/* Modal */} +
+

Purge Room

+

+ Are you sure you want to permanently delete this room and all its contents? + This action cannot be undone and will remove: +

+
    +
  • All code content
  • +
  • All comments
  • +
  • All uploaded files
  • +
  • Room history
  • +
+
+ + +
+
+
+ )} + {/* Comments Panel */} {showDisconnectToast && (
diff --git a/client/components/CommentsPanel.tsx b/client/components/CommentsPanel.tsx index 005ee31..34e4c7d 100644 --- a/client/components/CommentsPanel.tsx +++ b/client/components/CommentsPanel.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { MessageSquare } from 'lucide-react'; +import { MessageSquare, X } from 'lucide-react'; interface Comment { id: string; @@ -31,6 +31,7 @@ interface CommentsPageProps { onCommentSelect?: (lineNumber: number, lineRange?: string) => void; comments?: Comment[]; onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void; + onDeleteComment?: (commentId: string) => void; currentUser?: User | null; } @@ -41,6 +42,7 @@ export const CommentsPanel: React.FC = ({ onCommentSelect, comments = [], onAddComment, + onDeleteComment, currentUser }) => { const [newComment, setNewComment] = useState(''); @@ -139,7 +141,7 @@ export const CommentsPanel: React.FC = ({ .map((comment) => ( { if (comment.lineNumber && onCommentSelect) { onCommentSelect(comment.lineNumber, comment.lineRange); @@ -161,9 +163,23 @@ export const CommentsPanel: React.FC = ({ )}
- - {formatTime(comment.timestamp)} - +
+ + {formatTime(comment.timestamp)} + + {currentUser && comment.authorId === currentUser.id && onDeleteComment && ( + + )} +
diff --git a/client/components/RightPanel.tsx b/client/components/RightPanel.tsx index 01fbcaf..6042eb4 100644 --- a/client/components/RightPanel.tsx +++ b/client/components/RightPanel.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { MessageSquare } from 'lucide-react'; +import { MessageSquare, X } from 'lucide-react'; interface Comment { id: string; @@ -31,6 +31,7 @@ interface CommentsPageProps { onCommentSelect?: (lineNumber: number, lineRange?: string) => void; comments?: Comment[]; onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void; + onDeleteComment?: (commentId: string) => void; currentUser?: User | null; } @@ -41,6 +42,7 @@ export const CommentsPanel: React.FC = ({ onCommentSelect, comments = [], onAddComment, + onDeleteComment, currentUser }) => { const [newComment, setNewComment] = useState(''); @@ -139,7 +141,7 @@ export const CommentsPanel: React.FC = ({ .map((comment) => ( { if (comment.lineNumber && onCommentSelect) { onCommentSelect(comment.lineNumber, comment.lineRange); @@ -161,9 +163,23 @@ export const CommentsPanel: React.FC = ({ )}
- - {formatTime(comment.timestamp)} - +
+ + {formatTime(comment.timestamp)} + + {currentUser && onDeleteComment && ( + + )} +
diff --git a/server/main.go b/server/main.go index 0e0bac5..8294268 100644 --- a/server/main.go +++ b/server/main.go @@ -235,6 +235,7 @@ func main() { httpMux.HandleFunc("/o/upload", corsMiddleware(handleFileUpload)) httpMux.HandleFunc("/o/files/", corsMiddleware(handleFileServe)) httpMux.HandleFunc("/o/delete/", corsMiddleware(handleFileDelete)) + httpMux.HandleFunc("/purge/", corsMiddleware(handleRoomPurge)) http_port := 8090 @@ -419,7 +420,7 @@ func handleFileDelete(w http.ResponseWriter, r *http.Request) { } // Extract room code and file ID from URL path - path := r.URL.Path[10:] // Remove "/delete/" prefix + path := r.URL.Path[10:] // Remove "/o/delete/" prefix if path == "" { http.Error(w, "Invalid file path", http.StatusBadRequest) return @@ -486,6 +487,70 @@ func handleFileDelete(w http.ResponseWriter, r *http.Request) { w.Write([]byte("File deleted successfully")) } +func handleRoomPurge(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Extract room code from URL path + path := r.URL.Path[9:] // Remove "/purge/" prefix + if path == "" { + http.Error(w, "Room code required", http.StatusBadRequest) + return + } + + roomCode := path + + // First, disconnect all clients and remove room from memory + roomsMutex.Lock() + room, exists := rooms[roomCode] + if exists { + // Disconnect all clients + room.mutex.Lock() + for _, client := range room.Clients { + if client.Conn != nil { + client.Conn.Close() + } + } + room.mutex.Unlock() + + // Remove room from memory + delete(rooms, roomCode) + } + roomsMutex.Unlock() + + // Delete from database - room content + if err := deleteRoomContent(roomCode); err != nil { + log.Printf("Error deleting room content: %v", err) + http.Error(w, "Failed to delete room content", http.StatusInternalServerError) + return + } + + // Delete all comments for this room + if err := deleteRoomComments(roomCode); err != nil { + log.Printf("Error deleting room comments: %v", err) + http.Error(w, "Failed to delete room comments", http.StatusInternalServerError) + return + } + + // Delete all media files for this room + if err := deleteRoomMedia(roomCode); err != nil { + log.Printf("Error deleting room media files: %v", err) + http.Error(w, "Failed to delete room media files", http.StatusInternalServerError) + return + } + + // Delete physical files from filesystem + roomDir := filepath.Join(filesDir, roomCode) + if err := os.RemoveAll(roomDir); err != nil { + log.Printf("Warning: Could not delete room directory %s: %v", roomDir, err) + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Room purged successfully")) +} + func checkClientPings() { roomsMutex.RLock() defer roomsMutex.RUnlock() @@ -872,7 +937,44 @@ func handleCommentUpdate(conn *websocket.Conn, message []byte, currentRoom strin } func handleCommentDelete(conn *websocket.Conn, message []byte, currentRoom string, clientID string) { - // Similar implementation for comment deletion + if currentRoom == "" || clientID == "" { + return + } + + var commentMsg CommentMessage + if err := json.Unmarshal(message, &commentMsg); err != nil { + log.Printf("Error unmarshaling comment delete message: %v", err) + return + } + + roomsMutex.RLock() + room, exists := rooms[currentRoom] + roomsMutex.RUnlock() + + if !exists { + return + } + + // Allow anyone to delete comments - no authorization check needed + + // Delete from database + if err := deleteComment(commentMsg.Comment.ID); err != nil { + log.Printf("Error deleting comment from database: %v", err) + return + } + + // Remove from room + room.mutex.Lock() + for i, comment := range room.Comments { + if comment.ID == commentMsg.Comment.ID { + room.Comments = append(room.Comments[:i], room.Comments[i+1:]...) + break + } + } + room.mutex.Unlock() + + // Broadcast to all clients + broadcastToRoom(room, commentMsg, "") } func handleUserActivity(conn *websocket.Conn, message []byte, currentRoom string, clientID string) { @@ -1046,11 +1148,9 @@ func getRoomContent(code string) (string, error) { return content, nil } -func deleteRoomContent(code string) { +func deleteRoomContent(code string) error { _, err := db.Exec("DELETE FROM rooms WHERE code = ?", code) - if err != nil { - log.Printf("Error deleting room content for room %s: %v", code, err) - } + return err } func saveComment(roomCode string, comment Comment) error { @@ -1061,6 +1161,11 @@ func saveComment(roomCode string, comment Comment) error { return err } +func deleteComment(commentID string) error { + _, err := db.Exec(`DELETE FROM comments WHERE id = ?`, commentID) + return err +} + func getRoomComments(roomCode string) ([]Comment, error) { rows, err := db.Query(`SELECT id, line_number, line_range, author, author_id, content, timestamp FROM comments WHERE room_code = ? ORDER BY timestamp ASC`, roomCode) @@ -1083,11 +1188,9 @@ func getRoomComments(roomCode string) ([]Comment, error) { return comments, nil } -func deleteRoomComments(roomCode string) { +func deleteRoomComments(roomCode string) error { _, err := db.Exec("DELETE FROM comments WHERE room_code = ?", roomCode) - if err != nil { - log.Printf("Error deleting comments for room %s: %v", roomCode, err) - } + return err } func getRoomMedia(roomCode string) ([]MediaFile, error) { @@ -1112,11 +1215,9 @@ func getRoomMedia(roomCode string) ([]MediaFile, error) { return mediaFiles, nil } -func deleteRoomMedia(roomCode string) { +func deleteRoomMedia(roomCode string) error { _, err := db.Exec("DELETE FROM media_files WHERE room_code = ?", roomCode) - if err != nil { - log.Printf("Error deleting media files for room %s: %v", roomCode, err) - } + return err } func saveMediaFile(roomCode string, media MediaFile) error {