diff --git a/public/sounds/meeting-started.mp3 b/public/sounds/meeting-started.mp3 new file mode 100644 index 0000000..d3def7e Binary files /dev/null and b/public/sounds/meeting-started.mp3 differ diff --git a/src/i18n/de.json b/src/i18n/de.json index 71a23af..ab87360 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -212,6 +212,11 @@ "guestModeratorPlaceholder": "Nur wenn Sie Moderator sind", "guestJoinButton": "Meeting beitreten", "guestWaitingMessage": "Das Meeting wurde noch nicht gestartet. Bitte warten Sie, bis der Moderator es startet.", + "guestWaitingTitle": "Warte auf Meeting-Start...", + "guestWaitingHint": "Du wirst automatisch beigetreten, sobald das Meeting gestartet wird.", + "guestCancelWaiting": "Abbrechen", + "guestMeetingStartedJoining": "Meeting gestartet! Trete jetzt bei...", + "waitingToJoin": "Warten...", "guestAccessDenied": "Zugang nicht möglich", "guestNameRequired": "Name ist erforderlich", "guestJoinFailed": "Beitritt fehlgeschlagen", diff --git a/src/i18n/en.json b/src/i18n/en.json index df6833c..0da4e05 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -212,6 +212,11 @@ "guestModeratorPlaceholder": "Only if you are a moderator", "guestJoinButton": "Join meeting", "guestWaitingMessage": "The meeting has not started yet. Please wait for the moderator to start it.", + "guestWaitingTitle": "Waiting for meeting to start...", + "guestWaitingHint": "You will be joined automatically as soon as the meeting starts.", + "guestCancelWaiting": "Cancel", + "guestMeetingStartedJoining": "Meeting started! Joining now...", + "waitingToJoin": "Waiting...", "guestAccessDenied": "Access denied", "guestNameRequired": "Name is required", "guestJoinFailed": "Join failed", diff --git a/src/pages/GuestJoin.jsx b/src/pages/GuestJoin.jsx index 400a5c4..7fb360f 100644 --- a/src/pages/GuestJoin.jsx +++ b/src/pages/GuestJoin.jsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useParams, Link, useSearchParams } from 'react-router-dom'; -import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio, AlertCircle, FileText } from 'lucide-react'; +import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio, AlertCircle, FileText, Clock, X } from 'lucide-react'; import BrandLogo from '../components/BrandLogo'; import api from '../services/api'; import toast from 'react-hot-toast'; @@ -24,6 +24,33 @@ export default function GuestJoin() { const [moderatorCode, setModeratorCode] = useState(''); const [status, setStatus] = useState({ running: false }); const [recordingConsent, setRecordingConsent] = useState(false); + const [waiting, setWaiting] = useState(false); + const prevRunningRef = useRef(false); + + const joinMeeting = async () => { + setJoining(true); + try { + const res = await api.post(`/rooms/${uid}/guest-join`, { + name: name.trim(), + access_code: accessCode || undefined, + moderator_code: moderatorCode || undefined, + }); + if (res.data.joinUrl) { + window.location.href = res.data.joinUrl; + } + } catch (err) { + const errStatus = err.response?.status; + if (errStatus === 403) { + toast.error(t('room.guestWrongAccessCode')); + setWaiting(false); + } else { + toast.error(t('room.guestJoinFailed')); + setWaiting(false); + } + } finally { + setJoining(false); + } + }; useEffect(() => { const fetchRoom = async () => { @@ -31,6 +58,7 @@ export default function GuestJoin() { const res = await api.get(`/rooms/${uid}/public`); setRoomInfo(res.data.room); setStatus({ running: res.data.running }); + prevRunningRef.current = res.data.running; } catch (err) { const status = err.response?.status; if (status === 403) { @@ -53,45 +81,36 @@ export default function GuestJoin() { } catch { // ignore } - }, 10000); + }, 5000); return () => clearInterval(interval); }, [uid]); + // Auto-join when meeting starts while waiting + useEffect(() => { + if (!prevRunningRef.current && status.running && waiting) { + new Audio('/sounds/meeting-started.mp3').play().catch(() => {}); + toast.success(t('room.guestMeetingStartedJoining')); + joinMeeting(); + } + prevRunningRef.current = status.running; + }, [status.running]); + const handleJoin = async (e) => { e.preventDefault(); if (!name.trim()) { toast.error(t('room.guestNameRequired')); return; } - if (roomInfo?.allow_recording && !recordingConsent) { toast.error(t('room.guestRecordingConsent')); return; } - - setJoining(true); - try { - const res = await api.post(`/rooms/${uid}/guest-join`, { - name: name.trim(), - access_code: accessCode || undefined, - moderator_code: moderatorCode || undefined, - }); - if (res.data.joinUrl) { - window.location.href = res.data.joinUrl; - } - } catch (err) { - const status = err.response?.status; - if (status === 403) { - toast.error(t('room.guestWrongAccessCode')); - } else if (status === 400) { - toast.error(t('room.guestWaitingMessage')); - } else { - toast.error(t('room.guestJoinFailed')); - } - } finally { - setJoining(false); + if (!status.running) { + setWaiting(true); + return; } + await joinMeeting(); }; if (loading) { @@ -164,6 +183,33 @@ export default function GuestJoin() { {/* Join form */} + {waiting ? ( +
{t('room.guestWaitingTitle')}
+{t('room.guestWaitingHint')}
+