New federation features
All checks were successful
Build & Push Docker Image / build (push) Successful in 5m58s
All checks were successful
Build & Push Docker Image / build (push) Successful in 5m58s
This commit is contained in:
66
src/components/FederatedRoomCard.jsx
Normal file
66
src/components/FederatedRoomCard.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Globe, Play, Trash2, ExternalLink } from 'lucide-react';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import api from '../services/api';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function FederatedRoomCard({ room, onRemove }) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const handleJoin = () => {
|
||||
window.open(room.join_url, '_blank');
|
||||
};
|
||||
|
||||
const handleRemove = async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm(t('federation.removeRoomConfirm'))) return;
|
||||
try {
|
||||
await api.delete(`/federation/federated-rooms/${room.id}`);
|
||||
toast.success(t('federation.roomRemoved'));
|
||||
onRemove?.();
|
||||
} catch {
|
||||
toast.error(t('federation.roomRemoveFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-hover group p-5">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe size={14} className="text-th-accent flex-shrink-0" />
|
||||
<h3 className="text-base font-semibold text-th-text truncate group-hover:text-th-accent transition-colors">
|
||||
{room.room_name}
|
||||
</h3>
|
||||
<span className="flex-shrink-0 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-0.5 truncate">
|
||||
{t('federation.from')}: <span className="font-medium">{room.from_user}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Read-only notice */}
|
||||
<p className="text-xs text-th-text-s mb-4 italic">{t('federation.readOnlyNotice')}</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-3 border-t border-th-border">
|
||||
<button
|
||||
onClick={handleJoin}
|
||||
className="btn-primary text-xs py-1.5 px-3 flex-1"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{t('federation.joinMeeting')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className="btn-ghost text-xs py-1.5 px-2 text-th-error hover:text-th-error"
|
||||
title={t('federation.removeRoom')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -123,7 +123,9 @@
|
||||
"roomDeleteFailed": "Raum konnte nicht gelöscht werden",
|
||||
"roomDeleteConfirm": "Raum \"{name}\" wirklich löschen?",
|
||||
"loadFailed": "Räume konnten nicht geladen werden",
|
||||
"sharedWithMe": "Mit mir geteilt"
|
||||
"sharedWithMe": "Mit mir geteilt",
|
||||
"federatedRooms": "Räume von anderen Instanzen",
|
||||
"federatedRoomsSubtitle": "Angenommene Meeting-Einladungen von anderen Redlight-Instanzen. Einstellungen können hier nicht geändert werden."
|
||||
},
|
||||
"room": {
|
||||
"backToDashboard": "Zurück zum Dashboard",
|
||||
@@ -332,6 +334,14 @@
|
||||
"statusDeclined": "Abgelehnt",
|
||||
"openLink": "Meeting öffnen",
|
||||
"loadFailed": "Einladungen konnten nicht geladen werden",
|
||||
"inviteRemote": "Remote einladen"
|
||||
"inviteRemote": "Remote einladen",
|
||||
"federated": "Fremd-Instanz",
|
||||
"readOnlyNotice": "Dieser Raum gehört einer anderen Instanz. Einstellungen können nicht geändert werden.",
|
||||
"joinMeeting": "Meeting beitreten",
|
||||
"removeRoom": "Raum entfernen",
|
||||
"removeRoomConfirm": "Raum wirklich entfernen?",
|
||||
"roomRemoved": "Raum entfernt",
|
||||
"roomRemoveFailed": "Raum konnte nicht entfernt werden",
|
||||
"acceptedSaved": "Einladung angenommen – Raum wurde in deinem Dashboard gespeichert!"
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,9 @@
|
||||
"roomDeleteFailed": "Room could not be deleted",
|
||||
"roomDeleteConfirm": "Really delete room \"{name}\"?",
|
||||
"loadFailed": "Rooms could not be loaded",
|
||||
"sharedWithMe": "Shared with me"
|
||||
"sharedWithMe": "Shared with me",
|
||||
"federatedRooms": "Rooms from other instances",
|
||||
"federatedRoomsSubtitle": "Accepted meeting invitations from other Redlight instances. Settings cannot be changed here."
|
||||
},
|
||||
"room": {
|
||||
"backToDashboard": "Back to Dashboard",
|
||||
@@ -332,6 +334,14 @@
|
||||
"statusDeclined": "Declined",
|
||||
"openLink": "Open meeting",
|
||||
"loadFailed": "Could not load invitations",
|
||||
"inviteRemote": "Invite remote"
|
||||
"inviteRemote": "Invite remote",
|
||||
"federated": "Federated",
|
||||
"readOnlyNotice": "This room belongs to another instance. Settings cannot be changed.",
|
||||
"joinMeeting": "Join meeting",
|
||||
"removeRoom": "Remove room",
|
||||
"removeRoomConfirm": "Really remove this room?",
|
||||
"roomRemoved": "Room removed",
|
||||
"roomRemoveFailed": "Could not remove room",
|
||||
"acceptedSaved": "Invitation accepted – room saved to your dashboard!"
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ import { Plus, Video, Loader2, LayoutGrid, List } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import RoomCard from '../components/RoomCard';
|
||||
import FederatedRoomCard from '../components/FederatedRoomCard';
|
||||
import Modal from '../components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useLanguage();
|
||||
const [rooms, setRooms] = useState([]);
|
||||
const [federatedRooms, setFederatedRooms] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [viewMode, setViewMode] = useState('grid');
|
||||
@@ -33,8 +35,18 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFederatedRooms = async () => {
|
||||
try {
|
||||
const res = await api.get('/federation/federated-rooms');
|
||||
setFederatedRooms(res.data.rooms || []);
|
||||
} catch {
|
||||
// Federation may not be enabled
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRooms();
|
||||
fetchFederatedRooms();
|
||||
}, []);
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
@@ -160,6 +172,23 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Federated rooms (from other instances) */}
|
||||
{federatedRooms.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-lg font-semibold text-th-text mb-1">{t('dashboard.federatedRooms')}</h2>
|
||||
<p className="text-sm text-th-text-s mb-4">{t('dashboard.federatedRoomsSubtitle')}</p>
|
||||
<div className={
|
||||
viewMode === 'grid'
|
||||
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'
|
||||
: 'space-y-3'
|
||||
}>
|
||||
{federatedRooms.map(room => (
|
||||
<FederatedRoomCard key={room.id} room={room} onRemove={fetchFederatedRooms} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -26,11 +26,8 @@ export default function FederationInbox() {
|
||||
|
||||
const handleAccept = async (id) => {
|
||||
try {
|
||||
const res = await api.post(`/federation/invitations/${id}/accept`);
|
||||
if (res.data.join_url) {
|
||||
window.open(res.data.join_url, '_blank');
|
||||
}
|
||||
toast.success(t('federation.accepted'));
|
||||
await api.post(`/federation/invitations/${id}/accept`);
|
||||
toast.success(t('federation.acceptedSaved'));
|
||||
fetchInvitations();
|
||||
} catch {
|
||||
toast.error(t('federation.acceptFailed'));
|
||||
|
||||
Reference in New Issue
Block a user