mirror of
https://github.com/arkorty/Osborne.git
synced 2026-03-17 16:51:44 +00:00
feat purge button, delete comments
This commit is contained in:
@@ -208,6 +208,7 @@ const Room = () => {
|
|||||||
const [status, setStatus] = useState("Disconnected");
|
const [status, setStatus] = useState("Disconnected");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isPurgeModalOpen, setIsPurgeModalOpen] = useState(false);
|
||||||
const [showDisconnectToast, setShowDisconnectToast] = useState(false);
|
const [showDisconnectToast, setShowDisconnectToast] = useState(false);
|
||||||
const [currentThemeId, setCurrentThemeId] = useState("one-dark-pro");
|
const [currentThemeId, setCurrentThemeId] = useState("one-dark-pro");
|
||||||
const [selectedLineStart, setSelectedLineStart] = useState<number>();
|
const [selectedLineStart, setSelectedLineStart] = useState<number>();
|
||||||
@@ -656,11 +657,52 @@ const Room = () => {
|
|||||||
socketRef.current.send(JSON.stringify(message));
|
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) => {
|
const handleFileUpload = async (files: FileList) => {
|
||||||
if (!files || files.length === 0 || !currentUser) return;
|
if (!files || files.length === 0 || !currentUser) return;
|
||||||
|
|
||||||
const maxFileSize = 10 * 1024 * 1024; // 10MB in bytes
|
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++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
@@ -680,7 +722,7 @@ const Room = () => {
|
|||||||
formData.append("uploadedBy", currentUser.name);
|
formData.append("uploadedBy", currentUser.name);
|
||||||
|
|
||||||
// Upload file to HTTP server
|
// Upload file to HTTP server
|
||||||
const response = await fetch(`${httpUrl}/upload`, {
|
const response = await fetch(`${httpUrl}/o/upload`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
@@ -705,10 +747,10 @@ const Room = () => {
|
|||||||
const handleFileDelete = async (fileId: string) => {
|
const handleFileDelete = async (fileId: string) => {
|
||||||
if (!roomCode) return;
|
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 {
|
try {
|
||||||
const response = await fetch(`${httpUrl}/delete/${roomCode}/${fileId}`, {
|
const response = await fetch(`${httpUrl}/o/delete/${roomCode}/${fileId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -727,7 +769,7 @@ const Room = () => {
|
|||||||
|
|
||||||
const showPopup = (message: string, type: 'default' | 'warning' = 'default') => {
|
const showPopup = (message: string, type: 'default' | 'warning' = 'default') => {
|
||||||
setPopupMessage({text: message, type});
|
setPopupMessage({text: message, type});
|
||||||
setTimeout(() => setPopupMessage(null), 2000);
|
setTimeout(() => setPopupMessage(null), 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isClient) return null;
|
if (!isClient) return null;
|
||||||
@@ -748,27 +790,11 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center relative z-10 w-full h-full bg-card dark:bg-card shadow-md transition-all duration-300 ${
|
className={`flex flex-col items-center relative z-10 w-full h-full bg-card dark:bg-card shadow-md transition-all duration-300 ${
|
||||||
isModalOpen ? "blur-sm" : ""
|
isModalOpen || isPurgeModalOpen ? "blur-sm" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between p-1 w-full">
|
<div className="flex flex-row items-center justify-between p-1 w-full">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger>
|
|
||||||
<Button
|
|
||||||
className="text-foreground bg-secondary px-2 py-0 h-5 rounded-sm text-xs btn-micro"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(roomCode);
|
|
||||||
showPopup("Room code copied to clipboard!");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{roomCode}
|
|
||||||
</Button>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground">
|
|
||||||
copy room code: {roomCode}
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
<HoverCard>
|
<HoverCard>
|
||||||
<HoverCardTrigger>
|
<HoverCardTrigger>
|
||||||
<Button
|
<Button
|
||||||
@@ -782,10 +808,24 @@ const Room = () => {
|
|||||||
share
|
share
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground">
|
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground z-50">
|
||||||
copy link to this page
|
copy link to this page
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<Button
|
||||||
|
className="bg-destructive px-2 py-0 h-5 text-xs rounded-sm hover:bg-destructive/80 btn-micro"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setIsPurgeModalOpen(true)}
|
||||||
|
>
|
||||||
|
purge
|
||||||
|
</Button>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground z-50">
|
||||||
|
permanently delete this room and all its contents
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
<HoverCard>
|
<HoverCard>
|
||||||
<HoverCardTrigger>
|
<HoverCardTrigger>
|
||||||
<Button
|
<Button
|
||||||
@@ -796,7 +836,7 @@ const Room = () => {
|
|||||||
exit
|
exit
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground">
|
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground z-50">
|
||||||
return to home
|
return to home
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
@@ -814,7 +854,7 @@ const Room = () => {
|
|||||||
upload
|
upload
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground z-50">
|
||||||
upload files
|
upload files
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
@@ -832,7 +872,7 @@ const Room = () => {
|
|||||||
theme
|
theme
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground z-50">
|
||||||
{getThemeById(currentThemeId)?.name || "Switch theme"}
|
{getThemeById(currentThemeId)?.name || "Switch theme"}
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
@@ -849,7 +889,7 @@ const Room = () => {
|
|||||||
media
|
media
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground z-50">
|
||||||
toggle users & media panel
|
toggle users & media panel
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
@@ -862,7 +902,7 @@ const Room = () => {
|
|||||||
notes
|
notes
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground z-50">
|
||||||
toggle comments panel
|
toggle comments panel
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
@@ -924,6 +964,7 @@ const Room = () => {
|
|||||||
onCommentSelect={handleCommentSelect}
|
onCommentSelect={handleCommentSelect}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
onAddComment={handleAddComment}
|
onAddComment={handleAddComment}
|
||||||
|
onDeleteComment={handleDeleteComment}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -961,6 +1002,47 @@ const Room = () => {
|
|||||||
onModalStateChange={setIsModalOpen}
|
onModalStateChange={setIsModalOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Purge Confirmation Modal */}
|
||||||
|
{isPurgeModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* Blurred overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-background/60 backdrop-blur-sm transition-all duration-300"
|
||||||
|
onClick={() => setIsPurgeModalOpen(false)}
|
||||||
|
/>
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative bg-card border border-border rounded-lg shadow-lg p-6 max-w-md w-full mx-4 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-4">Purge Room</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-6">
|
||||||
|
Are you sure you want to permanently delete this room and all its contents?
|
||||||
|
This action cannot be undone and will remove:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-muted-foreground mb-6 list-disc list-inside space-y-1">
|
||||||
|
<li>All code content</li>
|
||||||
|
<li>All comments</li>
|
||||||
|
<li>All uploaded files</li>
|
||||||
|
<li>Room history</li>
|
||||||
|
</ul>
|
||||||
|
<div className="flex gap-3 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsPurgeModalOpen(false)}
|
||||||
|
className="text-sm text-foreground"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handlePurgeRoom}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
Purge Room
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Comments Panel */}
|
{/* Comments Panel */}
|
||||||
{showDisconnectToast && (
|
{showDisconnectToast && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
|
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { MessageSquare } from 'lucide-react';
|
import { MessageSquare, X } from 'lucide-react';
|
||||||
|
|
||||||
interface Comment {
|
interface Comment {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,6 +31,7 @@ interface CommentsPageProps {
|
|||||||
onCommentSelect?: (lineNumber: number, lineRange?: string) => void;
|
onCommentSelect?: (lineNumber: number, lineRange?: string) => void;
|
||||||
comments?: Comment[];
|
comments?: Comment[];
|
||||||
onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void;
|
onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void;
|
||||||
|
onDeleteComment?: (commentId: string) => void;
|
||||||
currentUser?: User | null;
|
currentUser?: User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
|||||||
onCommentSelect,
|
onCommentSelect,
|
||||||
comments = [],
|
comments = [],
|
||||||
onAddComment,
|
onAddComment,
|
||||||
|
onDeleteComment,
|
||||||
currentUser
|
currentUser
|
||||||
}) => {
|
}) => {
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
@@ -139,7 +141,7 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
|||||||
.map((comment) => (
|
.map((comment) => (
|
||||||
<Card
|
<Card
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
className="border-border hover:shadow-md transition-shadow cursor-pointer"
|
className="border-border hover:shadow-md transition-shadow cursor-pointer group"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (comment.lineNumber && onCommentSelect) {
|
if (comment.lineNumber && onCommentSelect) {
|
||||||
onCommentSelect(comment.lineNumber, comment.lineRange);
|
onCommentSelect(comment.lineNumber, comment.lineRange);
|
||||||
@@ -161,9 +163,23 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground">
|
<div className="flex items-center gap-1">
|
||||||
{formatTime(comment.timestamp)}
|
<span className="text-xs text-muted-foreground">
|
||||||
</span>
|
{formatTime(comment.timestamp)}
|
||||||
|
</span>
|
||||||
|
{currentUser && comment.authorId === currentUser.id && onDeleteComment && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteComment(comment.id);
|
||||||
|
}}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/20 rounded-sm"
|
||||||
|
title="Delete comment"
|
||||||
|
>
|
||||||
|
<X size={12} className="text-destructive" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { MessageSquare } from 'lucide-react';
|
import { MessageSquare, X } from 'lucide-react';
|
||||||
|
|
||||||
interface Comment {
|
interface Comment {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,6 +31,7 @@ interface CommentsPageProps {
|
|||||||
onCommentSelect?: (lineNumber: number, lineRange?: string) => void;
|
onCommentSelect?: (lineNumber: number, lineRange?: string) => void;
|
||||||
comments?: Comment[];
|
comments?: Comment[];
|
||||||
onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void;
|
onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void;
|
||||||
|
onDeleteComment?: (commentId: string) => void;
|
||||||
currentUser?: User | null;
|
currentUser?: User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
|||||||
onCommentSelect,
|
onCommentSelect,
|
||||||
comments = [],
|
comments = [],
|
||||||
onAddComment,
|
onAddComment,
|
||||||
|
onDeleteComment,
|
||||||
currentUser
|
currentUser
|
||||||
}) => {
|
}) => {
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
@@ -139,7 +141,7 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
|||||||
.map((comment) => (
|
.map((comment) => (
|
||||||
<Card
|
<Card
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
className="border-border hover:shadow-md transition-shadow cursor-pointer"
|
className="border-border hover:shadow-md transition-shadow cursor-pointer group"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (comment.lineNumber && onCommentSelect) {
|
if (comment.lineNumber && onCommentSelect) {
|
||||||
onCommentSelect(comment.lineNumber, comment.lineRange);
|
onCommentSelect(comment.lineNumber, comment.lineRange);
|
||||||
@@ -161,9 +163,23 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground">
|
<div className="flex items-center gap-1">
|
||||||
{formatTime(comment.timestamp)}
|
<span className="text-xs text-muted-foreground">
|
||||||
</span>
|
{formatTime(comment.timestamp)}
|
||||||
|
</span>
|
||||||
|
{currentUser && onDeleteComment && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteComment(comment.id);
|
||||||
|
}}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/20 rounded-sm"
|
||||||
|
title="Delete comment"
|
||||||
|
>
|
||||||
|
<X size={12} className="text-destructive" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
|
|||||||
129
server/main.go
129
server/main.go
@@ -235,6 +235,7 @@ func main() {
|
|||||||
httpMux.HandleFunc("/o/upload", corsMiddleware(handleFileUpload))
|
httpMux.HandleFunc("/o/upload", corsMiddleware(handleFileUpload))
|
||||||
httpMux.HandleFunc("/o/files/", corsMiddleware(handleFileServe))
|
httpMux.HandleFunc("/o/files/", corsMiddleware(handleFileServe))
|
||||||
httpMux.HandleFunc("/o/delete/", corsMiddleware(handleFileDelete))
|
httpMux.HandleFunc("/o/delete/", corsMiddleware(handleFileDelete))
|
||||||
|
httpMux.HandleFunc("/purge/", corsMiddleware(handleRoomPurge))
|
||||||
|
|
||||||
http_port := 8090
|
http_port := 8090
|
||||||
|
|
||||||
@@ -419,7 +420,7 @@ func handleFileDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract room code and file ID from URL path
|
// 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 == "" {
|
if path == "" {
|
||||||
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -486,6 +487,70 @@ func handleFileDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte("File deleted successfully"))
|
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() {
|
func checkClientPings() {
|
||||||
roomsMutex.RLock()
|
roomsMutex.RLock()
|
||||||
defer roomsMutex.RUnlock()
|
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) {
|
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) {
|
func handleUserActivity(conn *websocket.Conn, message []byte, currentRoom string, clientID string) {
|
||||||
@@ -1046,11 +1148,9 @@ func getRoomContent(code string) (string, error) {
|
|||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteRoomContent(code string) {
|
func deleteRoomContent(code string) error {
|
||||||
_, err := db.Exec("DELETE FROM rooms WHERE code = ?", code)
|
_, err := db.Exec("DELETE FROM rooms WHERE code = ?", code)
|
||||||
if err != nil {
|
return err
|
||||||
log.Printf("Error deleting room content for room %s: %v", code, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveComment(roomCode string, comment Comment) error {
|
func saveComment(roomCode string, comment Comment) error {
|
||||||
@@ -1061,6 +1161,11 @@ func saveComment(roomCode string, comment Comment) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteComment(commentID string) error {
|
||||||
|
_, err := db.Exec(`DELETE FROM comments WHERE id = ?`, commentID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func getRoomComments(roomCode string) ([]Comment, error) {
|
func getRoomComments(roomCode string) ([]Comment, error) {
|
||||||
rows, err := db.Query(`SELECT id, line_number, line_range, author, author_id, content, timestamp
|
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)
|
FROM comments WHERE room_code = ? ORDER BY timestamp ASC`, roomCode)
|
||||||
@@ -1083,11 +1188,9 @@ func getRoomComments(roomCode string) ([]Comment, error) {
|
|||||||
return comments, nil
|
return comments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteRoomComments(roomCode string) {
|
func deleteRoomComments(roomCode string) error {
|
||||||
_, err := db.Exec("DELETE FROM comments WHERE room_code = ?", roomCode)
|
_, err := db.Exec("DELETE FROM comments WHERE room_code = ?", roomCode)
|
||||||
if err != nil {
|
return err
|
||||||
log.Printf("Error deleting comments for room %s: %v", roomCode, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRoomMedia(roomCode string) ([]MediaFile, error) {
|
func getRoomMedia(roomCode string) ([]MediaFile, error) {
|
||||||
@@ -1112,11 +1215,9 @@ func getRoomMedia(roomCode string) ([]MediaFile, error) {
|
|||||||
return mediaFiles, nil
|
return mediaFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteRoomMedia(roomCode string) {
|
func deleteRoomMedia(roomCode string) error {
|
||||||
_, err := db.Exec("DELETE FROM media_files WHERE room_code = ?", roomCode)
|
_, err := db.Exec("DELETE FROM media_files WHERE room_code = ?", roomCode)
|
||||||
if err != nil {
|
return err
|
||||||
log.Printf("Error deleting media files for room %s: %v", roomCode, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveMediaFile(roomCode string, media MediaFile) error {
|
func saveMediaFile(roomCode string, media MediaFile) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user