diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index e297785..01549ad 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -80,7 +80,7 @@ export default function Navbar({ onMenuClick }) {
{user?.display_name || user?.name}
-
{user?.email}
+
@{user?.name}
diff --git a/src/i18n/de.json b/src/i18n/de.json
index 066b9bf..3e7fd5b 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -341,8 +341,8 @@
"inviteTitle": "Remote-Benutzer einladen",
"inviteSubtitle": "Einen Benutzer von einer anderen Redlight-Instanz zu diesem Meeting einladen.",
"addressLabel": "Benutzeradresse",
- "addressPlaceholder": "benutzer@andere-instanz.de",
- "addressHint": "Format: Benutzername@Domain der Redlight-Instanz",
+ "addressPlaceholder": "@benutzer@andere-instanz.com",
+ "addressHint": "Format: @Benutzername@Domain der Redlight-Instanz",
"messageLabel": "Nachricht (optional)",
"messagePlaceholder": "Hallo, ich lade dich zu unserem Meeting ein!",
"send": "Einladung senden",
@@ -376,6 +376,13 @@
"maxParticipants": "Max. Teilnehmer",
"recordingOn": "Aufnahme aktiviert",
"recordingOff": "Aufnahme deaktiviert",
- "unlimited": "Unbegrenzt"
+ "unlimited": "Unbegrenzt",
+ "backToDashboard": "Zurück zum Dashboard",
+ "participantLimit": "Teilnehmerlimit gesetzt",
+ "recordingLabel": "Aufnahme",
+ "recordingOnHint": "Meetings in diesem Raum können aufgezeichnet werden",
+ "recordingOffHint": "Meetings in diesem Raum werden nicht aufgezeichnet",
+ "roomDetails": "Raumdetails",
+ "joinUrl": "Beitritts-URL"
}
}
\ No newline at end of file
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d347b7f..cc12b1f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -341,8 +341,8 @@
"inviteTitle": "Invite Remote User",
"inviteSubtitle": "Invite a user from another Redlight instance to this meeting.",
"addressLabel": "User address",
- "addressPlaceholder": "user@other-instance.com",
- "addressHint": "Format: username@domain of the Redlight instance",
+ "addressPlaceholder": "@user@other-instance.com",
+ "addressHint": "Format: @username@domain of the Redlight instance",
"messageLabel": "Message (optional)",
"messagePlaceholder": "Hi, I'd like to invite you to our meeting!",
"send": "Send invitation",
@@ -376,6 +376,13 @@
"maxParticipants": "Max. participants",
"recordingOn": "Recording enabled",
"recordingOff": "Recording disabled",
- "unlimited": "Unlimited"
+ "unlimited": "Unlimited",
+ "backToDashboard": "Back to Dashboard",
+ "participantLimit": "Participant limit set",
+ "recordingLabel": "Recording",
+ "recordingOnHint": "Meetings in this room may be recorded",
+ "recordingOffHint": "Meetings in this room will not be recorded",
+ "roomDetails": "Room Details",
+ "joinUrl": "Join URL"
}
}
\ No newline at end of file
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 6f57c78..d5f7eab 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -130,7 +130,7 @@ export default function Dashboard() {
{/* Room grid/list */}
- {rooms.length === 0 ? (
+ {rooms.length === 0 && federatedRooms.length === 0 ? (
{t('dashboard.noRooms')}
diff --git a/src/pages/FederatedRoomDetail.jsx b/src/pages/FederatedRoomDetail.jsx
new file mode 100644
index 0000000..d24aded
--- /dev/null
+++ b/src/pages/FederatedRoomDetail.jsx
@@ -0,0 +1,183 @@
+import { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import {
+ ArrowLeft, Globe, ExternalLink, Trash2, Hash, Users,
+ Video, VideoOff, Loader2, Link2,
+} 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 = () => {
+ window.open(room.join_url, '_blank');
+ };
+
+ 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 (
+
+
+
+ );
+ }
+
+ if (!room) return null;
+
+ const recordingOn = room.allow_recording === 1 || room.allow_recording === true;
+
+ return (
+
+ {/* Back */}
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
{room.room_name}
+
+ {t('federation.federated')}
+
+
+
+ {t('federation.from')}: {room.from_user}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Info cards */}
+
+ {/* Max participants */}
+
+
+
+ {t('federation.maxParticipants')}
+
+
+ {room.max_participants > 0 ? room.max_participants : '∞'}
+
+
+ {room.max_participants > 0 ? t('federation.participantLimit') : t('federation.unlimited')}
+
+
+
+ {/* Recording */}
+
+
+ {recordingOn ? : }
+ {t('federation.recordingLabel')}
+
+
+ {recordingOn ? t('federation.recordingOn') : t('federation.recordingOff')}
+
+
+ {recordingOn ? t('federation.recordingOnHint') : t('federation.recordingOffHint')}
+
+
+
+
+ {/* Details */}
+
+
+ {t('federation.roomDetails')}
+
+
+ {room.meet_id && (
+
+
+
+
{t('federation.meetingId')}
+
{room.meet_id}
+
+
+ )}
+
+
+
+
+
{t('federation.joinUrl')}
+
{room.join_url}
+
+
+
+
+ {/* Read-only notice */}
+
+ {t('federation.readOnlyNotice')}
+
+
+ );
+}
diff --git a/src/pages/RoomDetail.jsx b/src/pages/RoomDetail.jsx
index bdf2264..a56a47b 100644
--- a/src/pages/RoomDetail.jsx
+++ b/src/pages/RoomDetail.jsx
@@ -216,7 +216,9 @@ export default function RoomDetail() {
const handleFedInvite = async (e) => {
e.preventDefault();
- if (!fedAddress.includes('@')) {
+ // 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;
}