import { useState, useEffect } from 'react'; import useLiveKit from './hooks/useLiveKit'; import usePush from './hooks/usePush'; import PTTButton from './components/PTTButton'; import UserList from './components/UserList'; import GroupSelector from './components/GroupSelector'; import Settings from './components/Settings'; import PWAInstallPrompt from './components/PWAInstallPrompt'; import './App.css'; const API_URL = import.meta.env.VITE_API_URL || '/api'; function App() { const [username, setUsername] = useState(''); const [groupId, setGroupId] = useState(''); const [groups, setGroups] = useState([]); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); const [showSettings, setShowSettings] = useState(false); const { isConnected, participants, isTalking, audioLevel, connect, disconnect, switchGroup, startTalking, stopTalking, toggleParticipantMute } = useLiveKit(); const { isSupported: isPushSupported, isPermissionGranted: isPushGranted, requestPermission: requestPushPermission, showNotification } = usePush(); // Charger configuration au démarrage useEffect(() => { fetch(`${API_URL}/config`) .then(res => res.json()) .then(data => { setGroups(data.groups || []); if (data.groups.length > 0) { setGroupId(data.groups[0].id); } }) .catch(err => { console.error('Erreur chargement config:', err); setError('Impossible de charger la configuration'); }); }, []); const handleConnect = async () => { if (!username.trim()) { setError('Veuillez entrer votre nom'); return; } if (!groupId) { setError('Veuillez sélectionner un groupe'); return; } setIsConnecting(true); setError(null); try { // Demander permission notifications au premier lancement if (isPushSupported && !isPushGranted) { console.log('Demande permission notifications...'); await requestPushPermission(); } // IMPORTANT iOS : Demander permission microphone AVANT tout console.log('🎤 Demande permission microphone...'); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); console.log('✓ Permission microphone accordée'); // Arrêter le stream test immédiatement stream.getTracks().forEach(track => track.stop()); } catch (permErr) { console.error('❌ Permission microphone refusée:', permErr); throw new Error('Accès microphone refusé. Autorisez dans les réglages iOS : Safari > Microphone.'); } // Obtenir token du serveur const response = await fetch(`${API_URL}/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, groupId }) }); if (!response.ok) { throw new Error('Erreur serveur'); } const data = await response.json(); // En mode dev (HTTPS via Vite), utiliser le proxy WebSocket // En mode prod (HTTP direct), utiliser l'URL LiveKit directement let livekitUrl = data.url; if (import.meta.env.DEV && window.location.protocol === 'https:') { // Mode dev avec Vite : utiliser le proxy WSS livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`; } console.log('🔗 Connexion LiveKit:', livekitUrl); console.log('📝 Mode:', import.meta.env.DEV ? 'dev' : 'prod'); // Se connecter à LiveKit avec les canaux virtuels await connect(livekitUrl, data.token, data.virtualChannels || []); } catch (err) { console.error('Erreur connexion:', err); // Message d'erreur spécifique selon le type if (err.message && err.message.includes('Microphone')) { setError(err.message); } else { setError('Connexion impossible. Vérifiez le serveur et les permissions microphone.'); } } finally { setIsConnecting(false); } }; const handleDisconnect = () => { disconnect(); setError(null); }; const handleGroupChange = async (newGroupId) => { console.log('🔄 Changement de groupe:', groupId, '→', newGroupId); try { // Obtenir nouveau token pour le nouveau groupe const response = await fetch(`${API_URL}/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, groupId: newGroupId }) }); if (!response.ok) { throw new Error('Erreur serveur'); } const data = await response.json(); // Adapter l'URL LiveKit selon le protocole de la page let livekitUrl = data.url; if (window.location.protocol === 'https:') { livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`; } // Changer de room LiveKit avec les canaux virtuels du nouveau groupe await switchGroup(livekitUrl, data.token, data.virtualChannels || []); // Mettre à jour l'état setGroupId(newGroupId); console.log('✓ Groupe changé avec succès'); } catch (err) { console.error('Erreur changement de groupe:', err); throw err; // Propager l'erreur au composant GroupSelector } }; // Interface de connexion if (!isConnected) { return (
Professional Intercom
{error && ({groups.find(g => g.id === groupId)?.name || groupId}