import { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { ArrowLeft, Play, Square, Users, Settings, FileVideo, Radio, Loader2, Copy, ExternalLink, Lock, Mic, MicOff, UserCheck, Shield, Save, UserPlus, X, Share2, Globe, Send, FileText, Upload, Trash2, } from 'lucide-react'; import Modal from '../components/Modal'; import api from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import RecordingList from '../components/RecordingList'; import toast from 'react-hot-toast'; export default function RoomDetail() { const { uid } = useParams(); const navigate = useNavigate(); const { user } = useAuth(); const { t } = useLanguage(); const [room, setRoom] = useState(null); const [status, setStatus] = useState({ running: false, participantCount: 0, moderatorCount: 0 }); const [recordings, setRecordings] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); const [activeTab, setActiveTab] = useState('overview'); const [editRoom, setEditRoom] = useState(null); const [saving, setSaving] = useState(false); const [sharedUsers, setSharedUsers] = useState([]); const [shareSearch, setShareSearch] = useState(''); const [shareResults, setShareResults] = useState([]); const [shareSearching, setShareSearching] = useState(false); // Federation invite state const [showFedInvite, setShowFedInvite] = useState(false); const [fedAddress, setFedAddress] = useState(''); const [fedMessage, setFedMessage] = useState(''); const [fedSending, setFedSending] = useState(false); // Presentation state const [uploadingPresentation, setUploadingPresentation] = useState(false); const [removingPresentation, setRemovingPresentation] = useState(false); const presentationInputRef = useRef(null); const isOwner = room && user && room.user_id === user.id; const isShared = room && !!room.shared; const canManage = isOwner || isShared; const fetchRoom = async () => { try { const res = await api.get(`/rooms/${uid}`); setRoom(res.data.room); setEditRoom(res.data.room); if (res.data.sharedUsers) setSharedUsers(res.data.sharedUsers); } catch { toast.error(t('room.notFound')); navigate('/dashboard'); } finally { setLoading(false); } }; const fetchStatus = async () => { try { const res = await api.get(`/rooms/${uid}/status`); setStatus(res.data); } catch { // Ignore } }; const fetchRecordings = async () => { try { const res = await api.get(`/recordings/room/${uid}`); setRecordings(res.data.recordings || []); } catch { // Ignore } }; useEffect(() => { fetchRoom(); fetchStatus(); fetchRecordings(); const interval = setInterval(fetchStatus, 10000); return () => clearInterval(interval); }, [uid]); const handleStart = async () => { setActionLoading('start'); try { const res = await api.post(`/rooms/${uid}/start`); if (res.data.joinUrl) { window.open(res.data.joinUrl, '_blank'); } setTimeout(fetchStatus, 2000); toast.success(t('room.meetingStarted')); } catch (err) { toast.error(err.response?.data?.error || t('room.meetingStartFailed')); } finally { setActionLoading(null); } }; const handleJoin = async () => { setActionLoading('join'); try { const data = room.access_code ? { access_code: prompt(t('room.enterAccessCode')) } : {}; const res = await api.post(`/rooms/${uid}/join`, data); if (res.data.joinUrl) { window.open(res.data.joinUrl, '_blank'); } } catch (err) { toast.error(err.response?.data?.error || t('room.joinFailed')); } finally { setActionLoading(null); } }; const handleEnd = async () => { if (!confirm(t('room.endConfirm'))) return; setActionLoading('end'); try { await api.post(`/rooms/${uid}/end`); toast.success(t('room.meetingEnded')); setTimeout(fetchStatus, 2000); } catch (err) { toast.error(err.response?.data?.error || t('room.meetingEndFailed')); } finally { setActionLoading(null); } }; const handleSaveSettings = async (e) => { e.preventDefault(); setSaving(true); try { const res = await api.put(`/rooms/${uid}`, { name: editRoom.name, welcome_message: editRoom.welcome_message, max_participants: editRoom.max_participants, access_code: editRoom.access_code, mute_on_join: !!editRoom.mute_on_join, require_approval: !!editRoom.require_approval, anyone_can_start: !!editRoom.anyone_can_start, all_join_moderator: !!editRoom.all_join_moderator, record_meeting: !!editRoom.record_meeting, guest_access: !!editRoom.guest_access, moderator_code: editRoom.moderator_code, }); setRoom(res.data.room); setEditRoom(res.data.room); toast.success(t('room.settingsSaved')); } catch (err) { toast.error(t('room.settingsSaveFailed')); } finally { setSaving(false); } }; const copyLink = () => { navigator.clipboard.writeText(`${window.location.origin}/rooms/${uid}`); toast.success(t('room.linkCopied')); }; // Federation invite handler const handlePresentationUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; const allowedTypes = [ 'application/pdf', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.presentation', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ]; if (!allowedTypes.includes(file.type)) { toast.error('Unsupported file type. Allowed: PDF, PPT, PPTX, ODP, DOC, DOCX'); return; } setUploadingPresentation(true); try { const arrayBuffer = await file.arrayBuffer(); const res = await api.post(`/rooms/${uid}/presentation`, arrayBuffer, { headers: { 'Content-Type': file.type, 'X-Filename': encodeURIComponent(file.name), }, }); setRoom(res.data.room); setEditRoom(res.data.room); toast.success(t('room.presentationUploaded')); } catch (err) { toast.error(err.response?.data?.error || t('room.presentationUploadFailed')); } finally { setUploadingPresentation(false); if (presentationInputRef.current) presentationInputRef.current.value = ''; } }; const handlePresentationRemove = async () => { setRemovingPresentation(true); try { const res = await api.delete(`/rooms/${uid}/presentation`); setRoom(res.data.room); setEditRoom(res.data.room); toast.success(t('room.presentationRemoved')); } catch (err) { toast.error(err.response?.data?.error || t('room.presentationRemoveFailed')); } finally { setRemovingPresentation(false); } }; const handleFedInvite = async (e) => { e.preventDefault(); // Accept @user@domain or user@domain — must have a domain part const normalized = fedAddress.startsWith('@') ? fedAddress.slice(1) : fedAddress; if (!normalized.includes('@') || normalized.endsWith('@')) { toast.error(t('federation.addressHint')); return; } setFedSending(true); try { await api.post('/federation/invite', { room_uid: uid, to: fedAddress, message: fedMessage || undefined, }); toast.success(t('federation.sent')); setShowFedInvite(false); setFedAddress(''); setFedMessage(''); } catch (err) { toast.error(err.response?.data?.error || t('federation.sendFailed')); } finally { setFedSending(false); } }; // Share functions const searchUsers = async (query) => { setShareSearch(query); if (query.length < 2) { setShareResults([]); return; } setShareSearching(true); try { const res = await api.get(`/rooms/users/search?q=${encodeURIComponent(query)}`); // Filter out already shared users const sharedIds = new Set(sharedUsers.map(u => u.id)); setShareResults(res.data.users.filter(u => !sharedIds.has(u.id))); } catch { setShareResults([]); } finally { setShareSearching(false); } }; const handleShare = async (userId) => { try { const res = await api.post(`/rooms/${uid}/shares`, { user_id: userId }); setSharedUsers(res.data.shares); setShareSearch(''); setShareResults([]); toast.success(t('room.shareAdded')); } catch (err) { toast.error(err.response?.data?.error || t('room.shareFailed')); } }; const handleUnshare = async (userId) => { try { const res = await api.delete(`/rooms/${uid}/shares/${userId}`); setSharedUsers(res.data.shares); toast.success(t('room.shareRemoved')); } catch { toast.error(t('room.shareFailed')); } }; if (loading) { return (
); } if (!room) return null; const tabs = [ { id: 'overview', label: t('room.overview'), icon: Play }, { id: 'recordings', label: t('room.recordings'), icon: FileVideo, count: recordings.length }, ...(isOwner ? [{ id: 'settings', label: t('room.settings'), icon: Settings }] : []), ]; return (
{/* Back button */} {/* Room header */}

{room.name}

{status.running && ( {t('common.live')} )}
{status.running ? t('room.participants', { count: status.participantCount }) : t('common.offline')} {room.access_code && ( {t('common.protected')} )}
{canManage && ( )} {canManage && !status.running && ( )} {status.running && ( )} {canManage && status.running && ( )}
{/* Tabs */}
{tabs.map(tab => ( ))}
{/* Tab content */} {activeTab === 'overview' && (
{/* Meeting info */}

{t('room.meetingDetails')}

{t('room.meetingId')} {room.uid}
{t('room.status')} {status.running ? t('common.active') : t('common.inactive')}
{t('room.maxParticipants')} {room.max_participants || t('common.unlimited')}
{t('room.accessCode')} {room.access_code || t('common.none')}
{/* Room settings overview */}

{t('room.roomSettings')}

{room.mute_on_join ? : } {room.mute_on_join ? t('room.mutedOnJoin') : t('room.micActiveOnJoin')}
{room.require_approval ? t('room.approvalRequired') : t('room.freeJoin')}
{room.all_join_moderator ? t('room.allModerators') : t('room.rolesAssigned')}
{room.record_meeting ? t('room.recordingAllowed') : t('room.recordingDisabled')}
{/* Welcome message */}

{t('room.welcomeMsg')}

{room.welcome_message || '—'}

)} {activeTab === 'recordings' && ( )} {activeTab === 'settings' && isOwner && editRoom && (
setEditRoom({ ...editRoom, name: e.target.value })} className="input-field" required />