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, Link, BarChart3, } 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 AnalyticsList from '../components/AnalyticsList'; 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 [analytics, setAnalytics] = 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); const [waitingToJoin, setWaitingToJoin] = useState(false); const prevRunningRef = useRef(false); const [showCopyMenu, setShowCopyMenu] = useState(false); const copyMenuRef = useRef(null); useEffect(() => { const handleClickOutside = (e) => { if (copyMenuRef.current && !copyMenuRef.current.contains(e.target)) { setShowCopyMenu(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // 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 } }; const fetchAnalytics = async () => { try { const res = await api.get(`/analytics/room/${uid}`); setAnalytics(res.data.analytics || []); } catch { // Ignore } }; useEffect(() => { fetchRoom(); fetchStatus(); fetchRecordings(); fetchAnalytics(); const interval = setInterval(fetchStatus, 10000); return () => clearInterval(interval); }, [uid]); // Auto-join when meeting starts while waiting useEffect(() => { if (!prevRunningRef.current && status.running && waitingToJoin) { new Audio('/sounds/meeting-started.mp3').play().catch(() => {}); toast.success(t('room.meetingStarted')); setWaitingToJoin(false); setActionLoading('join'); api.post(`/rooms/${uid}/join`, {}) .then(res => { if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank'); }) .catch(err => toast.error(err.response?.data?.error || t('room.joinFailed'))) .finally(() => setActionLoading(null)); } prevRunningRef.current = status.running; }, [status.running]); 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 () => { if (!status.running) { setWaitingToJoin(true); toast(t('room.guestWaitingTitle'), { icon: '🕐' }); return; } setWaitingToJoin(false); 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, learning_analytics: !!editRoom.learning_analytics, analytics_visibility: editRoom.analytics_visibility || 'owner', }); setRoom(res.data.room); setEditRoom(res.data.room); toast.success(t('room.settingsSaved')); } catch (err) { toast.error(t('room.settingsSaveFailed')); } finally { setSaving(false); } }; const copyToClipboard = (url) => { navigator.clipboard.writeText(url); toast.success(t('room.linkCopied')); setShowCopyMenu(false); }; // 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 (
{room.uid}
{room.welcome_message || '—'}
{t('federation.inviteSubtitle')}