All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s
216 lines
7.9 KiB
JavaScript
216 lines
7.9 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import {
|
|
ArrowLeft, Globe, ExternalLink, Trash2, Hash, Users,
|
|
Video, VideoOff, Loader2, Link2, AlertTriangle,
|
|
} from 'lucide-react';
|
|
import api from '../services/api';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import toast from 'react-hot-toast';
|
|
|
|
export default function FederatedRoomDetail() {
|
|
const { id } = useParams();
|
|
const navigate = useNavigate();
|
|
const { t } = useLanguage();
|
|
|
|
const [room, setRoom] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [removing, setRemoving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetch = async () => {
|
|
try {
|
|
const res = await api.get('/federation/federated-rooms');
|
|
const found = (res.data.rooms || []).find(r => String(r.id) === String(id));
|
|
if (!found) {
|
|
toast.error(t('room.notFound'));
|
|
navigate('/dashboard');
|
|
return;
|
|
}
|
|
setRoom(found);
|
|
} catch {
|
|
toast.error(t('room.notFound'));
|
|
navigate('/dashboard');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetch();
|
|
}, [id]);
|
|
|
|
const handleJoin = () => {
|
|
// Validate URL scheme to prevent javascript: or other malicious URIs
|
|
try {
|
|
const url = new URL(room.join_url);
|
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
toast.error(t('federation.invalidJoinUrl'));
|
|
return;
|
|
}
|
|
window.open(room.join_url, '_blank');
|
|
} catch {
|
|
toast.error(t('federation.invalidJoinUrl'));
|
|
}
|
|
};
|
|
|
|
const handleRemove = async () => {
|
|
if (!confirm(t('federation.removeRoomConfirm'))) return;
|
|
setRemoving(true);
|
|
try {
|
|
await api.delete(`/federation/federated-rooms/${room.id}`);
|
|
toast.success(t('federation.roomRemoved'));
|
|
navigate('/dashboard');
|
|
} catch {
|
|
toast.error(t('federation.roomRemoveFailed'));
|
|
setRemoving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 size={32} className="animate-spin text-th-accent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!room) return null;
|
|
|
|
const recordingOn = room.allow_recording === 1 || room.allow_recording === true;
|
|
const isDeleted = room.deleted === 1 || room.deleted === true;
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto">
|
|
{/* Back */}
|
|
<button
|
|
onClick={() => navigate('/dashboard')}
|
|
className="flex items-center gap-2 text-sm text-th-text-s hover:text-th-text transition-colors mb-6"
|
|
>
|
|
<ArrowLeft size={16} />
|
|
{t('federation.backToDashboard')}
|
|
</button>
|
|
|
|
{/* Deleted banner */}
|
|
{isDeleted && (
|
|
<div className="card p-4 mb-4 border-red-500/30 bg-red-500/10">
|
|
<div className="flex items-center gap-3">
|
|
<AlertTriangle size={20} className="text-red-500 flex-shrink-0" />
|
|
<div>
|
|
<p className="text-sm font-semibold text-red-500">{t('federation.roomDeleted')}</p>
|
|
<p className="text-xs text-th-text-s mt-0.5">{t('federation.roomDeletedNotice')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header */}
|
|
<div className="card p-6 mb-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
|
<div className="flex items-start gap-3 min-w-0">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5 ${isDeleted ? 'bg-red-500/15' : 'bg-th-accent/15'}`}>
|
|
{isDeleted ? <AlertTriangle size={20} className="text-red-500" /> : <Globe size={20} className="text-th-accent" />}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h1 className="text-xl font-bold text-th-text truncate">{room.room_name}</h1>
|
|
{isDeleted ? (
|
|
<span className="px-2 py-0.5 bg-red-500/15 text-red-500 rounded-full text-xs font-medium">
|
|
{t('federation.roomDeleted')}
|
|
</span>
|
|
) : (
|
|
<span className="px-2 py-0.5 bg-th-accent/15 text-th-accent rounded-full text-xs font-medium">
|
|
{t('federation.federated')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-th-text-s mt-1">
|
|
{t('federation.from')}: <span className="font-medium text-th-text">{room.from_user}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{!isDeleted && (
|
|
<button
|
|
onClick={handleJoin}
|
|
className="btn-primary"
|
|
>
|
|
<ExternalLink size={16} />
|
|
{t('federation.joinMeeting')}
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={handleRemove}
|
|
disabled={removing}
|
|
className="btn-ghost text-th-error hover:text-th-error px-3"
|
|
title={t('federation.removeRoom')}
|
|
>
|
|
{removing ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
|
{/* Max participants */}
|
|
<div className="card p-5">
|
|
<div className="flex items-center gap-2 mb-1 text-xs font-semibold text-th-text-s uppercase tracking-wider">
|
|
<Users size={13} />
|
|
{t('federation.maxParticipants')}
|
|
</div>
|
|
<p className="text-2xl font-bold text-th-text">
|
|
{room.max_participants > 0 ? room.max_participants : '∞'}
|
|
</p>
|
|
<p className="text-xs text-th-text-s mt-0.5">
|
|
{room.max_participants > 0 ? t('federation.participantLimit') : t('federation.unlimited')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Recording */}
|
|
<div className="card p-5">
|
|
<div className="flex items-center gap-2 mb-1 text-xs font-semibold text-th-text-s uppercase tracking-wider">
|
|
{recordingOn ? <Video size={13} /> : <VideoOff size={13} />}
|
|
{t('federation.recordingLabel')}
|
|
</div>
|
|
<p className={`text-lg font-bold ${recordingOn ? 'text-amber-400' : 'text-th-text-s'}`}>
|
|
{recordingOn ? t('federation.recordingOn') : t('federation.recordingOff')}
|
|
</p>
|
|
<p className="text-xs text-th-text-s mt-0.5">
|
|
{recordingOn ? t('federation.recordingOnHint') : t('federation.recordingOffHint')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Details */}
|
|
<div className="card p-5 space-y-4">
|
|
<h2 className="text-sm font-semibold text-th-text uppercase tracking-wider">
|
|
{t('federation.roomDetails')}
|
|
</h2>
|
|
|
|
{room.meet_id && (
|
|
<div className="flex items-start gap-3">
|
|
<Hash size={16} className="text-th-accent flex-shrink-0 mt-0.5" />
|
|
<div className="min-w-0">
|
|
<p className="text-xs text-th-text-s mb-0.5">{t('federation.meetingId')}</p>
|
|
<p className="text-sm font-mono text-th-text break-all">{room.meet_id}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-start gap-3">
|
|
<Link2 size={16} className="text-th-accent flex-shrink-0 mt-0.5" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs text-th-text-s mb-0.5">{t('federation.joinUrl')}</p>
|
|
<p className="text-sm font-mono text-th-text break-all opacity-60 select-all">{room.join_url}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Read-only notice */}
|
|
<p className="text-xs text-th-text-s mt-4 text-center italic">
|
|
{isDeleted ? t('federation.roomDeletedNotice') : t('federation.readOnlyNotice')}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|