[call-me] - feat: UX overhaul — calling overlays, sign-in redesign, empty states, i18n updates, update dep

This commit is contained in:
Miroslav Pejic
2026-04-06 15:43:20 +02:00
parent faa44fb379
commit 70a7bd107e
20 changed files with 1051 additions and 136 deletions
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - مكالمات فيديو فورية",
"appDescription": "خيارك للمكالمات المرئية الفورية!",
"signIn": {
"title": "تسجيل الدخول",
"username": "أدخل اسم المستخدم",
"button": "تسجيل الدخول",
"title": "انضمام",
"username": "اختر اسم العرض",
"button": "انضمام",
"camera": "الكاميرا",
"microphone": "الميكروفون",
"enterUsername": "يرجى إدخال اسم المستخدم"
"enterUsername": "يرجى إدخال اسم العرض",
"subtitle": "مكالمات فيديو فورية. لا حاجة للتسجيل.",
"directCallLabel": "أو اتصل بشخص مباشرة",
"directCallPlaceholder": "أدخل اسمًا للاتصال",
"directCallButton": "اتصل الآن"
},
"room": {
"sessionTime": "مدة الجلسة",
@@ -27,7 +31,18 @@
"hangupWith": "إنهاء المكالمة مع __username__",
"callUser": "اتصل بـ __username__",
"videoOff": "الفيديو متوقف",
"videoDisabled": "الفيديو معطّل"
"videoDisabled": "الفيديو معطّل",
"noUsersOnline": "لا يوجد مستخدمون آخرون متصلون بعد",
"shareToInvite": "شارك رابط الاتصال لدعوة شخص ما!",
"noChatMessages": "لا توجد رسائل بعد. ابدأ المحادثة بعد الاتصال!",
"noActiveCall": "اختر مستخدمًا لبدء مكالمة فيديو",
"callingOverlay": "جارٍ الاتصال...",
"incomingCall": "مكالمة واردة",
"cancelCall": "إلغاء",
"callDeclined": "رفض __username__ المكالمة",
"callBusy": "__username__ في مكالمة أخرى",
"callTimeout": "لا يوجد رد من __username__",
"usersOnline": "__count__ مستخدم(ين) متصل"
},
"controls": {
"microphone": "الميكروفون",
@@ -60,8 +75,8 @@
"error": "حدث خطأ",
"copied": "تم النسخ إلى الحافظة",
"invalidPassword": "كلمة مرور غير صحيحة",
"shareRoomText": "انضم إلى غرفة Call-me الخاصة بي!",
"roomCopied": "تم نسخ الغرفة إلى الحافظة __text__",
"shareRoomText": "اتصل بي على Call-me!",
"roomCopied": "تم نسخ رابط الاتصال إلى الحافظة!",
"devicesRefreshed": "تم تحديث الأجهزة بنجاح",
"cameraChanged": "تم تغيير الكاميرا بنجاح",
"microphoneChanged": "تم تغيير الميكروفون بنجاح",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Sofortige Videoanrufe",
"appDescription": "Ihre erste Wahl für sofortige Videoanrufe!",
"signIn": {
"title": "Anmelden",
"username": "Benutzernamen eingeben",
"button": "Anmelden",
"title": "Beitreten",
"username": "Anzeigename wählen",
"button": "Beitreten",
"camera": "Kamera",
"microphone": "Mikrofon",
"enterUsername": "Bitte geben Sie Ihren Benutzernamen ein"
"enterUsername": "Bitte geben Sie Ihren Anzeigenamen ein",
"subtitle": "Sofortige Videoanrufe. Keine Anmeldung nötig.",
"directCallLabel": "Oder jemanden direkt anrufen",
"directCallPlaceholder": "Namen zum Anrufen eingeben",
"directCallButton": "Jetzt anrufen"
},
"room": {
"sessionTime": "Sitzungszeit",
@@ -27,7 +31,18 @@
"hangupWith": "Anruf mit __username__ auflegen",
"callUser": "__username__ anrufen",
"videoOff": "Video aus",
"videoDisabled": "Video deaktiviert"
"videoDisabled": "Video deaktiviert",
"noUsersOnline": "Noch keine anderen Benutzer online",
"shareToInvite": "Teile deinen Anruflink, um jemanden einzuladen!",
"noChatMessages": "Noch keine Nachrichten. Starte einen Chat nach der Verbindung!",
"noActiveCall": "Wähle einen Benutzer, um einen Videoanruf zu starten",
"callingOverlay": "Anruf läuft...",
"incomingCall": "Eingehender Anruf",
"cancelCall": "Abbrechen",
"callDeclined": "__username__ hat den Anruf abgelehnt",
"callBusy": "__username__ führt bereits ein anderes Gespräch",
"callTimeout": "Keine Antwort von __username__",
"usersOnline": "__count__ Benutzer online"
},
"controls": {
"microphone": "Mikrofon",
@@ -60,8 +75,8 @@
"error": "Ein Fehler ist aufgetreten",
"copied": "In die Zwischenablage kopiert",
"invalidPassword": "Ungültiges Passwort",
"shareRoomText": "Tritt meinem Call-me-Raum bei!",
"roomCopied": "Raum in die Zwischenablage kopiert __text__",
"shareRoomText": "Ruf mich auf Call-me an!",
"roomCopied": "Anruflink in die Zwischenablage kopiert!",
"devicesRefreshed": "Geräte erfolgreich aktualisiert",
"cameraChanged": "Kamera erfolgreich gewechselt",
"microphoneChanged": "Mikrofon erfolgreich gewechselt",
+23 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Instant Video Calls",
"appDescription": "Your Go-To for Instant Video Calls!",
"signIn": {
"title": "Sign In",
"username": "Enter username",
"button": "Sign In",
"title": "Join",
"username": "Choose a display name",
"button": "Join",
"camera": "Camera",
"microphone": "Microphone",
"enterUsername": "Please enter your username"
"enterUsername": "Please enter your display name",
"subtitle": "Instant Video Calls. No signup needed.",
"directCallLabel": "Or call someone directly",
"directCallPlaceholder": "Enter name to call",
"directCallButton": "Call Now"
},
"room": {
"sessionTime": "Session Time",
@@ -27,7 +31,19 @@
"hangupWith": "Hang up call with __username__",
"callUser": "Call __username__",
"videoOff": "Video Off",
"videoDisabled": "Video Disabled"
"videoDisabled": "Video Disabled",
"noUsersOnline": "No other users online yet",
"shareToInvite": "Share your call link to invite someone!",
"noChatMessages": "No messages yet. Start chatting once connected!",
"noActiveCall": "Select a user to start a video call",
"callingOverlay": "Calling...",
"incomingCall": "Incoming call",
"cancelCall": "Cancel",
"callDeclined": "__username__ declined the call",
"callBusy": "__username__ is on another call",
"callTimeout": "No answer from __username__",
"connecting": "Connecting...",
"usersOnline": "__count__ user(s) online"
},
"controls": {
"microphone": "Microphone",
@@ -60,8 +76,8 @@
"error": "An error occurred",
"copied": "Copied to clipboard",
"invalidPassword": "Invalid password",
"shareRoomText": "Join my Call-me room!",
"roomCopied": "Room copied to clipboard __text__",
"shareRoomText": "Call me on Call-me!",
"roomCopied": "Call link copied to clipboard!",
"devicesRefreshed": "Devices refreshed successfully",
"cameraChanged": "Camera changed successfully",
"microphoneChanged": "Microphone changed successfully",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Videollamadas Instantáneas",
"appDescription": "¡Tu opción para videollamadas instantáneas!",
"signIn": {
"title": "Iniciar Sesión",
"username": "Ingrese nombre de usuario",
"button": "Iniciar Sesión",
"title": "Unirse",
"username": "Elige un nombre para mostrar",
"button": "Unirse",
"camera": "Cámara",
"microphone": "Micrófono",
"enterUsername": "Por favor ingrese su nombre de usuario"
"enterUsername": "Por favor ingrese su nombre para mostrar",
"subtitle": "Videollamadas instantáneas. Sin registro.",
"directCallLabel": "O llama a alguien directamente",
"directCallPlaceholder": "Ingresa un nombre para llamar",
"directCallButton": "Llamar ahora"
},
"room": {
"sessionTime": "Tiempo de Sesión",
@@ -27,7 +31,18 @@
"hangupWith": "Colgar la llamada con __username__",
"callUser": "Llamar a __username__",
"videoOff": "Vídeo desactivado",
"videoDisabled": "Vídeo deshabilitado"
"videoDisabled": "Vídeo deshabilitado",
"noUsersOnline": "Aún no hay otros usuarios en línea",
"shareToInvite": "¡Comparte tu enlace de llamada para invitar a alguien!",
"noChatMessages": "¡Aún no hay mensajes. Empieza a chatear una vez conectado!",
"noActiveCall": "Selecciona un usuario para iniciar una videollamada",
"callingOverlay": "Llamando...",
"incomingCall": "Llamada entrante",
"cancelCall": "Cancelar",
"callDeclined": "__username__ rechazó la llamada",
"callBusy": "__username__ está en otra llamada",
"callTimeout": "Sin respuesta de __username__",
"usersOnline": "__count__ usuario(s) en línea"
},
"controls": {
"microphone": "Micrófono",
@@ -60,8 +75,8 @@
"error": "Ocurrió un error",
"copied": "Copiado al portapapeles",
"invalidPassword": "Contraseña inválida",
"shareRoomText": Únete a mi sala de Call-me!",
"roomCopied": "Sala copiada al portapapeles __text__",
"shareRoomText": Llámame en Call-me!",
"roomCopied": "¡Enlace de llamada copiado al portapapeles!",
"devicesRefreshed": "Dispositivos actualizados correctamente",
"cameraChanged": "Cámara cambiada correctamente",
"microphoneChanged": "Micrófono cambiado correctamente",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Appels Vidéo Instantanés",
"appDescription": "Votre solution pour les appels vidéo instantanés!",
"signIn": {
"title": "Se Connecter",
"username": "Entrez le nom d'utilisateur",
"button": "Se Connecter",
"title": "Rejoindre",
"username": "Choisissez un nom d'affichage",
"button": "Rejoindre",
"camera": "Caméra",
"microphone": "Microphone",
"enterUsername": "Veuillez entrer votre nom d'utilisateur"
"enterUsername": "Veuillez entrer votre nom d'affichage",
"subtitle": "Appels vidéo instantanés. Aucune inscription nécessaire.",
"directCallLabel": "Ou appelez quelqu'un directement",
"directCallPlaceholder": "Entrez un nom à appeler",
"directCallButton": "Appeler maintenant"
},
"room": {
"sessionTime": "Temps de Session",
@@ -27,7 +31,18 @@
"hangupWith": "Raccrocher avec __username__",
"callUser": "Appeler __username__",
"videoOff": "Vidéo coupée",
"videoDisabled": "Vidéo désactivée"
"videoDisabled": "Vidéo désactivée",
"noUsersOnline": "Aucun autre utilisateur en ligne pour le moment",
"shareToInvite": "Partagez votre lien d'appel pour inviter quelqu'un !",
"noChatMessages": "Pas encore de messages. Commencez à discuter une fois connecté !",
"noActiveCall": "Sélectionnez un utilisateur pour démarrer un appel vidéo",
"callingOverlay": "Appel en cours...",
"incomingCall": "Appel entrant",
"cancelCall": "Annuler",
"callDeclined": "__username__ a refusé l'appel",
"callBusy": "__username__ est déjà en ligne",
"callTimeout": "Pas de réponse de __username__",
"usersOnline": "__count__ utilisateur(s) en ligne"
},
"controls": {
"microphone": "Microphone",
@@ -60,8 +75,8 @@
"error": "Une erreur s'est produite",
"copied": "Copié dans le presse-papiers",
"invalidPassword": "Mot de passe invalide",
"shareRoomText": "Rejoignez ma salle Call-me !",
"roomCopied": "Salle copiée dans le presse-papiers __text__",
"shareRoomText": "Appelez-moi sur Call-me !",
"roomCopied": "Lien d'appel copié dans le presse-papiers !",
"devicesRefreshed": "Appareils actualisés avec succès",
"cameraChanged": "Caméra changée avec succès",
"microphoneChanged": "Microphone changé avec succès",
+23 -8
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - तुरंत वीडियो कॉल",
"appDescription": "तुरंत वीडियो कॉल के लिए आपका पसंदीदा विकल्प!",
"signIn": {
"title": "साइन इन",
"username": "उपयोगकर्ता नाम दर्ज करें",
"button": "साइन इन",
"title": "शामिल हों",
"username": "एक डिस्प्ले नाम चुनें",
"button": "शामिल हों",
"camera": "कैमरा",
"microphone": "माइक्रोफ़ोन",
"enterUsername": "कृपया अपना उपयोगकर्ता नाम दर्ज करें"
"microphone": "माइक्रोोन",
"enterUsername": "कृपया अपना डिस्प्ले नाम दर्ज करें",
"subtitle": "तुरंत वीडियो कॉल। साइनअप की ज़रूरत नहीं।",
"directCallLabel": "या किसी को सीधे कॉल करें",
"directCallPlaceholder": "कॉल करने के लिए नाम दर्ज करें",
"directCallButton": "अभी कॉल करें"
},
"room": {
"sessionTime": "सत्र समय",
@@ -27,7 +31,18 @@
"hangupWith": "__username__ के साथ कॉल समाप्त करें",
"callUser": "__username__ को कॉल करें",
"videoOff": "वीडियो बंद",
"videoDisabled": "वीडियो अक्षम"
"videoDisabled": "वीडियो अक्षम",
"noUsersOnline": "अभी कोई अन्य उपयोगकर्ता ऑनलाइन नहीं है",
"shareToInvite": "किसी को आमंत्रित करने के लिए अपना कॉल लिंक साझा करें!",
"noChatMessages": "अभी कोई संदेश नहीं। कनेक्ट होने के बाद चैट शुरू करें!",
"noActiveCall": "वीडियो कॉल शुरू करने के लिए एक उपयोगकर्ता चुनें",
"callingOverlay": "कॉल हो रहा है...",
"incomingCall": "आने वाली कॉल",
"cancelCall": "रद्द करें",
"callDeclined": "__username__ ने कॉल अस्वीकार कर दी",
"callBusy": "__username__ दूसरी कॉल पर है",
"callTimeout": "__username__ से कोई जवाब नहीं",
"usersOnline": "__count__ उपयोगकर्ता ऑनलाइन"
},
"controls": {
"microphone": "माइक्रोफ़ोन",
@@ -60,8 +75,8 @@
"error": "एक त्रुटि हुई",
"copied": "क्लिपबोर्ड पर कॉपी किया गया",
"invalidPassword": "अमान्य पासवर्ड",
"shareRoomText": "मेरे Call-me रूम में शामिल हों!",
"roomCopied": "रूम क्लिपबोर्ड पर कॉपी किया गया __text__",
"shareRoomText": "मुझे Call-me पर कॉल करें!",
"roomCopied": "कॉल लिंक क्लिपबोर्ड पर कॉपी किया गया!",
"devicesRefreshed": "डिवाइस सफलतापूर्वक रिफ्रेश किए गए",
"cameraChanged": "कैमरा सफलतापूर्वक बदला गया",
"microphoneChanged": "माइक्रोफ़ोन सफलतापूर्वक बदला गया",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Trenutni video pozivi",
"appDescription": "Vaš izbor za trenutne video pozive!",
"signIn": {
"title": "Prijava",
"username": "Unesite korisničko ime",
"button": "Prijavi se",
"title": "Pridruži se",
"username": "Odaberite ime za prikaz",
"button": "Pridruži se",
"camera": "Kamera",
"microphone": "Mikrofon",
"enterUsername": "Molimo unesite svoje korisničko ime"
"enterUsername": "Molimo unesite svoje ime za prikaz",
"subtitle": "Trenutni video pozivi. Bez registracije.",
"directCallLabel": "Ili nazovite nekoga izravno",
"directCallPlaceholder": "Unesite ime za poziv",
"directCallButton": "Pozovi odmah"
},
"room": {
"sessionTime": "Trajanje sesije",
@@ -27,7 +31,18 @@
"hangupWith": "Prekini poziv s __username__",
"callUser": "Nazovi __username__",
"videoOff": "Video isključen",
"videoDisabled": "Video onemogućen"
"videoDisabled": "Video onemogućen",
"noUsersOnline": "Još nema drugih korisnika na mreži",
"shareToInvite": "Podijelite svoj link za poziv da pozovete nekoga!",
"noChatMessages": "Još nema poruka. Počnite razgovarati nakon povezivanja!",
"noActiveCall": "Odaberite korisnika za pokretanje video poziva",
"callingOverlay": "Pozivanje...",
"incomingCall": "Dolazni poziv",
"cancelCall": "Odustani",
"callDeclined": "__username__ je odbio poziv",
"callBusy": "__username__ je na drugom pozivu",
"callTimeout": "Nema odgovora od __username__",
"usersOnline": "__count__ korisnik(a) na mreži"
},
"controls": {
"microphone": "Mikrofon",
@@ -60,8 +75,8 @@
"error": "Došlo je do pogreške",
"copied": "Kopirano u međuspremnik",
"invalidPassword": "Neispravna lozinka",
"shareRoomText": "Pridruži se mojoj Call-me sobi!",
"roomCopied": "Soba kopirana u međuspremnik __text__",
"shareRoomText": "Nazovi me na Call-me!",
"roomCopied": "Link za poziv kopiran u međuspremnik!",
"devicesRefreshed": "Uređaji su uspješno osvježeni",
"cameraChanged": "Kamera je uspješno promijenjena",
"microphoneChanged": "Mikrofon je uspješno promijenjen",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Videochiamate Istantanee",
"appDescription": "La tua scelta per videochiamate istantanee!",
"signIn": {
"title": "Accedi",
"username": "Inserisci nome utente",
"button": "Accedi",
"title": "Entra",
"username": "Scegli un nome visualizzato",
"button": "Entra",
"camera": "Fotocamera",
"microphone": "Microfono",
"enterUsername": "Inserisci il tuo nome utente"
"enterUsername": "Inserisci il tuo nome visualizzato",
"subtitle": "Videochiamate istantanee. Nessuna registrazione necessaria.",
"directCallLabel": "Oppure chiama qualcuno direttamente",
"directCallPlaceholder": "Inserisci un nome da chiamare",
"directCallButton": "Chiama ora"
},
"room": {
"sessionTime": "Tempo di Sessione",
@@ -27,7 +31,18 @@
"hangupWith": "Riaggancia la chiamata con __username__",
"callUser": "Chiama __username__",
"videoOff": "Video disattivato",
"videoDisabled": "Video disabilitato"
"videoDisabled": "Video disabilitato",
"noUsersOnline": "Nessun altro utente online al momento",
"shareToInvite": "Condividi il tuo link di chiamata per invitare qualcuno!",
"noChatMessages": "Nessun messaggio ancora. Inizia a chattare una volta connesso!",
"noActiveCall": "Seleziona un utente per avviare una videochiamata",
"callingOverlay": "Chiamata in corso...",
"incomingCall": "Chiamata in arrivo",
"cancelCall": "Annulla",
"callDeclined": "__username__ ha rifiutato la chiamata",
"callBusy": "__username__ è in un'altra chiamata",
"callTimeout": "Nessuna risposta da __username__",
"usersOnline": "__count__ utente/i online"
},
"controls": {
"microphone": "Microfono",
@@ -60,8 +75,8 @@
"error": "Si è verificato un errore",
"copied": "Copiato negli appunti",
"invalidPassword": "Password non valida",
"shareRoomText": "Unisciti alla mia stanza Call-me!",
"roomCopied": "Stanza copiata negli appunti __text__",
"shareRoomText": "Chiamami su Call-me!",
"roomCopied": "Link di chiamata copiato negli appunti!",
"devicesRefreshed": "Dispositivi aggiornati correttamente",
"cameraChanged": "Fotocamera cambiata correttamente",
"microphoneChanged": "Microfono cambiato correttamente",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - 即時ビデオ通話",
"appDescription": "即時ビデオ通話に最適!",
"signIn": {
"title": "サインイン",
"username": "ユーザー名を入力",
"button": "サインイン",
"title": "参加",
"username": "表示名を選択",
"button": "参加",
"camera": "カメラ",
"microphone": "マイク",
"enterUsername": "ユーザー名を入力してください"
"enterUsername": "表示名を入力してください",
"subtitle": "即時ビデオ通話。登録不要。",
"directCallLabel": "または直接電話をかける",
"directCallPlaceholder": "通話相手の名前を入力",
"directCallButton": "今すぐ電話"
},
"room": {
"sessionTime": "セッション時間",
@@ -27,7 +31,18 @@
"hangupWith": "__username__ との通話を終了",
"callUser": "__username__ に発信",
"videoOff": "ビデオオフ",
"videoDisabled": "ビデオ無効"
"videoDisabled": "ビデオ無効",
"noUsersOnline": "他のユーザーはまだオンラインではありません",
"shareToInvite": "通話リンクを共有して招待しましょう!",
"noChatMessages": "まだメッセージはありません。接続後にチャットを始めましょう!",
"noActiveCall": "ユーザーを選択してビデオ通話を開始",
"callingOverlay": "発信中...",
"incomingCall": "着信",
"cancelCall": "キャンセル",
"callDeclined": "__username__ が通話を拒否しました",
"callBusy": "__username__ は他の通話中です",
"callTimeout": "__username__ から応答がありません",
"usersOnline": "__count__ 人のユーザーがオンライン"
},
"controls": {
"microphone": "マイク",
@@ -60,8 +75,8 @@
"error": "エラーが発生しました",
"copied": "クリップボードにコピーしました",
"invalidPassword": "パスワードが無効です",
"shareRoomText": "私のCall-meルームに参加して!",
"roomCopied": "ルームをクリップボードにコピーしました __text__",
"shareRoomText": "Call-meで電話して",
"roomCopied": "通話リンクをクリップボードにコピーしました",
"devicesRefreshed": "デバイスを正常に更新しました",
"cameraChanged": "カメラを正常に変更しました",
"microphoneChanged": "マイクを正常に変更しました",
+20 -5
View File
@@ -4,11 +4,15 @@
"appDescription": "Sua opção para chamadas de vídeo instantâneas!",
"signIn": {
"title": "Entrar",
"username": "Digite o nome de usuário",
"username": "Escolha um nome de exibição",
"button": "Entrar",
"camera": "Câmera",
"microphone": "Microfone",
"enterUsername": "Por favor, digite seu nome de usuário"
"enterUsername": "Por favor, digite seu nome de exibição",
"subtitle": "Chamadas de vídeo instantâneas. Sem cadastro.",
"directCallLabel": "Ou ligue para alguém diretamente",
"directCallPlaceholder": "Digite um nome para ligar",
"directCallButton": "Ligar agora"
},
"room": {
"sessionTime": "Tempo da sessão",
@@ -27,7 +31,18 @@
"hangupWith": "Encerrar chamada com __username__",
"callUser": "Ligar para __username__",
"videoOff": "Vídeo desligado",
"videoDisabled": "Vídeo desativado"
"videoDisabled": "Vídeo desativado",
"noUsersOnline": "Nenhum outro usuário online ainda",
"shareToInvite": "Compartilhe seu link de chamada para convidar alguém!",
"noChatMessages": "Nenhuma mensagem ainda. Comece a conversar após conectar!",
"noActiveCall": "Selecione um usuário para iniciar uma chamada de vídeo",
"callingOverlay": "Ligando...",
"incomingCall": "Chamada recebida",
"cancelCall": "Cancelar",
"callDeclined": "__username__ recusou a chamada",
"callBusy": "__username__ está em outra chamada",
"callTimeout": "Sem resposta de __username__",
"usersOnline": "__count__ usuário(s) online"
},
"controls": {
"microphone": "Microfone",
@@ -60,8 +75,8 @@
"error": "Ocorreu um erro",
"copied": "Copiado para a área de transferência",
"invalidPassword": "Senha inválida",
"shareRoomText": "Entre na minha sala Call-me!",
"roomCopied": "Sala copiada para a área de transferência __text__",
"shareRoomText": "Me ligue no Call-me!",
"roomCopied": "Link de chamada copiado para a área de transferência!",
"devicesRefreshed": "Dispositivos atualizados com sucesso",
"cameraChanged": "Câmera alterada com sucesso",
"microphoneChanged": "Microfone alterado com sucesso",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me — мгновенные видеозвонки",
"appDescription": "Ваш выбор для мгновенных видеозвонков!",
"signIn": {
"title": "Войти",
"username": "Введите имя пользователя",
"button": "Войти",
"title": "Присоединиться",
"username": "Выберите отображаемое имя",
"button": "Присоединиться",
"camera": "Камера",
"microphone": "Микрофон",
"enterUsername": "Пожалуйста, введите имя пользователя"
"enterUsername": "Пожалуйста, введите отображаемое имя",
"subtitle": "Мгновенные видеозвонки. Регистрация не нужна.",
"directCallLabel": "Или позвоните кому-то напрямую",
"directCallPlaceholder": "Введите имя для звонка",
"directCallButton": "Позвонить сейчас"
},
"room": {
"sessionTime": "Время сессии",
@@ -27,7 +31,18 @@
"hangupWith": "Завершить звонок с __username__",
"callUser": "Позвонить __username__",
"videoOff": "Видео выключено",
"videoDisabled": "Видео отключено"
"videoDisabled": "Видео отключено",
"noUsersOnline": "Пока нет других пользователей онлайн",
"shareToInvite": "Поделитесь ссылкой для приглашения!",
"noChatMessages": "Сообщений пока нет. Начните общение после подключения!",
"noActiveCall": "Выберите пользователя для начала видеозвонка",
"callingOverlay": "Вызов...",
"incomingCall": "Входящий звонок",
"cancelCall": "Отмена",
"callDeclined": "__username__ отклонил звонок",
"callBusy": "__username__ уже на другом звонке",
"callTimeout": "Нет ответа от __username__",
"usersOnline": "__count__ пользователь(ей) онлайн"
},
"controls": {
"microphone": "Микрофон",
@@ -60,8 +75,8 @@
"error": "Произошла ошибка",
"copied": "Скопировано в буфер обмена",
"invalidPassword": "Неверный пароль",
"shareRoomText": рисоединяйтесь к моей комнате Call-me!",
"roomCopied": "Комната скопирована в буфер обмена __text__",
"shareRoomText": озвоните мне на Call-me!",
"roomCopied": "Ссылка для звонка скопирована в буфер обмена!",
"devicesRefreshed": "Устройства успешно обновлены",
"cameraChanged": "Камера успешно изменена",
"microphoneChanged": "Микрофон успешно изменён",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - Trenutni video pozivi",
"appDescription": "Vaš izbor za trenutne video pozive!",
"signIn": {
"title": "Prijava",
"username": "Unesite korisničko ime",
"button": "Prijavi se",
"title": "Pridruži se",
"username": "Izaberite ime za prikaz",
"button": "Pridruži se",
"camera": "Камера",
"microphone": "Микрофон",
"enterUsername": "Molimo unesite svoje korisničko ime"
"enterUsername": "Molimo unesite svoje ime za prikaz",
"subtitle": "Trenutni video pozivi. Bez registracije.",
"directCallLabel": "Ili pozovite nekoga direktno",
"directCallPlaceholder": "Unesite ime za poziv",
"directCallButton": "Pozovi odmah"
},
"room": {
"sessionTime": "Trajanje sesije",
@@ -27,7 +31,18 @@
"hangupWith": "Prekini poziv sa __username__",
"callUser": "Pozovi __username__",
"videoOff": "Video isključen",
"videoDisabled": "Video onemogućen"
"videoDisabled": "Video onemogućen",
"noUsersOnline": "Još nema drugih korisnika na mreži",
"shareToInvite": "Podelite svoj link za poziv da pozovete nekoga!",
"noChatMessages": "Još nema poruka. Počnite da ćaskate nakon povezivanja!",
"noActiveCall": "Izaberite korisnika za pokretanje video poziva",
"callingOverlay": "Pozivanje...",
"incomingCall": "Dolazni poziv",
"cancelCall": "Otkaži",
"callDeclined": "__username__ je odbio poziv",
"callBusy": "__username__ je na drugom pozivu",
"callTimeout": "Nema odgovora od __username__",
"usersOnline": "__count__ korisnik(a) na mreži"
},
"controls": {
"microphone": "Mikrofon",
@@ -60,8 +75,8 @@
"error": "Došlo je do greške",
"copied": "Kopirano u ostavu",
"invalidPassword": "Neispravna lozinka",
"shareRoomText": "Pridruži se mojoj Call-me sobi!",
"roomCopied": "Soba kopirana u ostavu __text__",
"shareRoomText": "Pozovi me na Call-me!",
"roomCopied": "Link za poziv kopiran u ostavu!",
"devicesRefreshed": "Uređaji su uspešno osveženi",
"cameraChanged": "Kamera je uspešno promenjena",
"microphoneChanged": "Mikrofon je uspešno promenjen",
+22 -7
View File
@@ -3,12 +3,16 @@
"appTitle": "Call-me - 即时视频通话",
"appDescription": "即时视频通话的首选!",
"signIn": {
"title": "登录",
"username": "请输入用户名",
"button": "登录",
"title": "加入",
"username": "选择显示名称",
"button": "加入",
"camera": "摄像头",
"microphone": "麦克风",
"enterUsername": "请输入您的用户名"
"enterUsername": "请输入您的显示名称",
"subtitle": "即时视频通话。无需注册。",
"directCallLabel": "或直接呼叫某人",
"directCallPlaceholder": "输入要呼叫的名称",
"directCallButton": "立即通话"
},
"room": {
"sessionTime": "会话时长",
@@ -27,7 +31,18 @@
"hangupWith": "挂断与 __username__ 的通话",
"callUser": "呼叫 __username__",
"videoOff": "视频已关闭",
"videoDisabled": "视频已禁用"
"videoDisabled": "视频已禁用",
"noUsersOnline": "目前没有其他用户在线",
"shareToInvite": "分享您的通话链接来邀请他人!",
"noChatMessages": "还没有消息。连接后开始聊天!",
"noActiveCall": "选择一个用户开始视频通话",
"callingOverlay": "正在呼叫...",
"incomingCall": "来电",
"cancelCall": "取消",
"callDeclined": "__username__ 拒绝了通话",
"callBusy": "__username__ 正在其他通话中",
"callTimeout": "__username__ 没有应答",
"usersOnline": "__count__ 个用户在线"
},
"controls": {
"microphone": "麦克风",
@@ -60,8 +75,8 @@
"error": "发生错误",
"copied": "已复制到剪贴板",
"invalidPassword": "密码无效",
"shareRoomText": "加入我的 Call-me 房间",
"roomCopied": "房间已复制到剪贴板 __text__",
"shareRoomText": " Call-me 上呼叫我",
"roomCopied": "通话链接已复制到剪贴板",
"devicesRefreshed": "设备刷新成功",
"cameraChanged": "摄像头切换成功",
"microphoneChanged": "麦克风切换成功",
+8 -4
View File
@@ -261,9 +261,9 @@ app.get('/join/', (req, res) => {
return unauthorized(res);
}
const isValidCall = isValidUsername(user);
const isValidCall = call ? isValidUsername(call) : true;
log.debug('isValidCall', { call: call, valid: isValidCall });
if (!isValidCall) {
if (call && !isValidCall) {
return unauthorized(res);
}
@@ -515,11 +515,15 @@ function handleConnection(socket) {
break;
case 'offerDecline':
log.warn(`User ${name} declined your call`);
sendError(recipientSocket || socket, `User ${name} declined your call`);
if (recipientSocket) {
sendMsgTo(recipientSocket, { type: 'offerDecline', from: name });
}
break;
case 'offerBusy':
log.warn(`User ${name} busy in another call`);
sendError(recipientSocket || socket, `User ${name} busy in another call.`);
if (recipientSocket) {
sendMsgTo(recipientSocket, { type: 'offerBusy', from: name });
}
break;
default:
log.warn(`Unknown offer type: ${type}`);
+4 -4
View File
@@ -13,7 +13,7 @@
"axios": "^1.14.0",
"colors": "^1.4.0",
"cors": "^2.8.6",
"dotenv": "^17.4.0",
"dotenv": "^17.4.1",
"express": "^5.2.1",
"helmet": "^8.1.0",
"httpolyglot": "0.1.2",
@@ -609,9 +609,9 @@
}
},
"node_modules/dotenv": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz",
"integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==",
"version": "17.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
+1 -1
View File
@@ -23,7 +23,7 @@
"axios": "^1.14.0",
"colors": "^1.4.0",
"cors": "^2.8.6",
"dotenv": "^17.4.0",
"dotenv": "^17.4.1",
"express": "^5.2.1",
"helmet": "^8.1.0",
"httpolyglot": "0.1.2",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

+224 -32
View File
@@ -61,6 +61,18 @@ const remoteVideoDisabled = document.getElementById('remoteVideoDisabled');
const localUsername = document.getElementById('localUsername');
const remoteUsername = document.getElementById('remoteUsername');
const remoteVideo = document.getElementById('remoteVideo');
const directCallInput = document.getElementById('directCallInput');
const directCallBtn = document.getElementById('directCallBtn');
const callingOverlay = document.getElementById('callingOverlay');
const callingUsername = document.getElementById('callingUsername');
const callingTimer = document.getElementById('callingTimer');
const cancelCallBtn = document.getElementById('cancelCallBtn');
const incomingCallOverlay = document.getElementById('incomingCallOverlay');
const incomingCallUsername = document.getElementById('incomingCallUsername');
const incomingCallTimer = document.getElementById('incomingCallTimer');
const acceptCallBtn = document.getElementById('acceptCallBtn');
const declineCallBtn = document.getElementById('declineCallBtn');
// Ensure app is defined, even if config.js is not loaded
const app = window.myAppConfig || {};
@@ -70,6 +82,10 @@ let userInfo;
let userName;
let connectedUser;
let pendingUser; // Track outgoing call target
let callingTimerId = null; // Timer for calling overlay
let callingElapsed = 0; // Seconds elapsed while calling
let incomingCallData = null; // Pending incoming call data
let incomingCallTimerId = null; // Auto-decline timer for incoming call
let thisConnection;
let camera = 'user';
let stream;
@@ -449,6 +465,12 @@ function handleMessage(data) {
}
offerCreate();
break;
case 'offerDecline':
handleOfferDecline(data);
break;
case 'offerBusy':
handleOfferBusy(data);
break;
case 'offer':
handleOffer(data);
break;
@@ -534,6 +556,14 @@ function handleListeners() {
emojiBtn.addEventListener('click', handleEmojiClick);
saveChatBtn.addEventListener('click', handleSaveChatClick);
clearChatBtn.addEventListener('click', handleClearChatClick);
// Direct call listeners
directCallBtn.addEventListener('click', handleDirectCallClick);
directCallInput.addEventListener('keyup', (e) => handleKeyUp(e, handleDirectCallClick));
cancelCallBtn.addEventListener('click', handleCancelCall);
acceptCallBtn.addEventListener('click', handleAcceptIncomingCall);
declineCallBtn.addEventListener('click', handleDeclineIncomingCall);
// Language change event listener - reapply config after translation
window.addEventListener('languageChanged', () => {
handleConfig();
@@ -645,7 +675,7 @@ function handleUserClickToCall(user) {
from: userName,
to: user,
});
toast(t('room.callingUser', { username: user }));
showCallingOverlay(user);
if (userSidebar.classList.contains('active')) {
userSidebar.classList.remove('active');
}
@@ -676,6 +706,75 @@ function handleSignInClick() {
}
}
// Handle direct call button click (sign in + auto-call)
function handleDirectCallClick() {
const myName = usernameIn.value.trim();
const callTarget = directCallInput ? directCallInput.value.trim() : '';
if (!myName) {
handleError(t('signIn.enterUsername'));
usernameIn.focus();
return;
}
if (!callTarget) {
handleError(t('errors.noUserSelected'));
if (directCallInput) directCallInput.focus();
return;
}
// Store call target for after sign-in
userName = myName;
localStorage.callMeUsername = myName;
sendMsg({
type: 'signIn',
name: myName,
});
// After sign-in completes, auto-call the target user
const waitForSignIn = setInterval(() => {
if (userSignedIn) {
clearInterval(waitForSignIn);
setTimeout(() => handleUserClickToCall(callTarget), 1500);
}
}, 200);
// Safety timeout to avoid infinite loop
setTimeout(() => clearInterval(waitForSignIn), 15000);
}
// Show calling overlay
function showCallingOverlay(targetUser) {
if (!callingOverlay) return;
callingElapsed = 0;
if (callingUsername) callingUsername.textContent = targetUser;
if (callingTimer) callingTimer.textContent = '0s';
callingOverlay.style.display = 'flex';
if (callingTimerId) clearInterval(callingTimerId);
callingTimerId = setInterval(() => {
callingElapsed++;
if (callingTimer) callingTimer.textContent = callingElapsed + 's';
}, 1000);
}
// Hide calling overlay
function hideCallingOverlay() {
if (!callingOverlay) return;
callingOverlay.style.display = 'none';
if (callingTimerId) {
clearInterval(callingTimerId);
callingTimerId = null;
}
callingElapsed = 0;
}
// Handle cancel call button click
function handleCancelCall() {
hideCallingOverlay();
if (pendingUser) {
// Notify the remote user that the call was cancelled
sendMsg({ type: 'leave', name: pendingUser });
pendingUser = null;
}
toast(t('messages.callEnded'), 'info', 'top', 2000);
}
// Share Room click handler
async function handleShareRoomClick() {
const roomUrl = window.location.origin;
@@ -1231,6 +1330,7 @@ function handlePing(data) {
// Handle user not found from the server
function handleNotFound(data) {
const { username } = data;
hideCallingOverlay();
handleError(t('errors.userNotFound', { username }));
// Remove from user list if present
allConnectedUsers = allConnectedUsers.filter((u) => u !== username);
@@ -1238,6 +1338,24 @@ function handleNotFound(data) {
updateParticipantCount();
}
// Handle call declined by remote user
function handleOfferDecline(data) {
const { from } = data;
hideCallingOverlay();
pendingUser = null;
toast(t('room.callDeclined', { username: from }), 'warning', 'top', 4000);
sound('notify');
}
// Handle remote user busy in another call
function handleOfferBusy(data) {
const { from } = data;
hideCallingOverlay();
pendingUser = null;
toast(t('room.callBusy', { username: from }), 'warning', 'top', 4000);
sound('notify');
}
// Handle sign-in response from the server
async function handleSignIn(data) {
const { success, message } = data;
@@ -1499,39 +1617,69 @@ function offerAccept(data) {
return;
}
Swal.fire({
heightAuto: false,
scrollbarPadding: false,
position: 'top',
imageUrl: 'assets/ring.png',
imageWidth: 284,
imageHeight: 120,
text: t('room.acceptCallFrom', { username: data.from }),
showDenyButton: true,
confirmButtonText: t('common.yes'),
denyButtonText: t('common.no'),
timerProgressBar: true,
timer: 10000,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
// Apply caller's media status before accepting the call
if (data.callerMediaStatus) {
applyCallerMediaStatus(data.callerMediaStatus);
}
data.type = 'offerCreate';
socket.recipient = data.from;
} else {
data.type = 'offerDecline';
}
sendMsg({ ...data });
});
incomingCallData = data;
showIncomingCallOverlay(data.from);
sound('ring');
}
// Show incoming call overlay
function showIncomingCallOverlay(callerName) {
if (!incomingCallOverlay) return;
if (incomingCallUsername) incomingCallUsername.textContent = callerName;
// Reset timer bar animation
if (incomingCallTimer) {
incomingCallTimer.style.animation = 'none';
incomingCallTimer.offsetHeight; // Force reflow
incomingCallTimer.style.animation = '';
}
incomingCallOverlay.style.display = 'flex';
// Auto-decline after 10 seconds
if (incomingCallTimerId) clearTimeout(incomingCallTimerId);
incomingCallTimerId = setTimeout(() => {
handleDeclineIncomingCall();
}, 10000);
}
// Hide incoming call overlay
function hideIncomingCallOverlay() {
if (!incomingCallOverlay) return;
incomingCallOverlay.style.display = 'none';
if (incomingCallTimerId) {
clearTimeout(incomingCallTimerId);
incomingCallTimerId = null;
}
incomingCallData = null;
}
// Handle accept incoming call
function handleAcceptIncomingCall() {
if (!incomingCallData) return;
const data = incomingCallData;
hideIncomingCallOverlay();
// Apply caller's media status before accepting the call
if (data.callerMediaStatus) {
applyCallerMediaStatus(data.callerMediaStatus);
}
data.type = 'offerCreate';
socket.recipient = data.from;
sendMsg({ ...data });
}
// Handle decline incoming call
function handleDeclineIncomingCall() {
if (!incomingCallData) return;
const data = incomingCallData;
hideIncomingCallOverlay();
data.type = 'offerDecline';
sendMsg({ ...data });
}
// Handle incoming offer
async function handleOffer(data) {
const { offer, name } = data;
@@ -1562,6 +1710,7 @@ async function handleAnswer(data) {
const { answer } = data;
try {
await thisConnection.setRemoteDescription(new RTCSessionDescription(answer));
hideCallingOverlay();
// Set connectedUser from pendingUser after call is accepted
if (pendingUser) {
connectedUser = pendingUser;
@@ -1570,6 +1719,7 @@ async function handleAnswer(data) {
renderUserList(); // Update UI to show hang-up button for caller
}
} catch (error) {
hideCallingOverlay();
handleError(t('errors.remoteDescriptionFailed'), error);
}
}
@@ -1723,6 +1873,8 @@ function disconnectConnection() {
// Handle leaving the room
function handleLeave(disconnect = true) {
hideCallingOverlay();
hideIncomingCallOverlay();
if (disconnect) {
// Stop screen sharing if active
if (isScreenSharing) {
@@ -2169,6 +2321,22 @@ function renderUserList() {
if (tip) tip.dispose();
});
userList.innerHTML = '';
// Show empty state if no users (only after sign-in)
if (filteredUsers.length === 0 && userSignedIn) {
const emptyLi = document.createElement('li');
emptyLi.className = 'user-list-empty';
emptyLi.innerHTML = `
<div>
<i class="fas fa-users"></i>
<p>${t('room.noUsersOnline')}</p>
<p>${t('room.shareToInvite')}</p>
</div>
`;
userList.appendChild(emptyLi);
return;
}
filteredUsers.forEach((user) => {
const li = document.createElement('li');
li.tabIndex = 0;
@@ -2309,6 +2477,10 @@ function switchTab(tabName) {
// Clear unread messages when switching to chat
unreadMessages = 0;
updateChatNotification();
// Show empty state if no messages
if (chatMessages && chatMessages.children.length === 0) {
showChatEmptyState();
}
} else if (tabName === 'settings') {
settingsTab.classList.add('active');
// Refresh devices when switching to settings
@@ -2376,6 +2548,10 @@ if (chatForm && chatInput) {
}
function addChatMessage(msg, isSelf = false) {
// Remove empty state if present
const emptyState = chatMessages.querySelector('.chat-empty-state');
if (emptyState) emptyState.remove();
const div = document.createElement('div');
div.className = 'chat-message';
if (isSelf) {
@@ -2790,7 +2966,23 @@ function handleClearChatClick() {
}
function thereAreChatMessages() {
return chatMessages.children.length > 0;
// Exclude the empty state element from the count
const children = Array.from(chatMessages.children);
return children.some((el) => !el.classList.contains('chat-empty-state'));
}
// Show chat empty state placeholder
function showChatEmptyState() {
if (!chatMessages) return;
const existing = chatMessages.querySelector('.chat-empty-state');
if (existing) return;
const emptyDiv = document.createElement('div');
emptyDiv.className = 'chat-empty-state';
emptyDiv.innerHTML = `
<i class="fas fa-comments"></i>
<p>${t('room.noChatMessages')}</p>
`;
chatMessages.appendChild(emptyDiv);
}
// Device Management Functions
+64 -3
View File
@@ -68,7 +68,12 @@
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header"><h1 id="appName" data-i18n="appName">Call-me</h1></div>
<div class="card-header">
<h1 id="appName" data-i18n="appName">Call-me</h1>
<p class="sign-in-subtitle" data-i18n="signIn.subtitle">
Instant Video Calls. No signup needed.
</p>
</div>
<div class="card-body">
<!-- Sign-in Form -->
<div class="mb-3">
@@ -76,7 +81,7 @@
<input
id="usernameIn"
type="text"
placeholder="Enter username"
placeholder="Choose a display name"
data-i18n-placeholder="signIn.username"
required
/>
@@ -105,7 +110,27 @@
</label>
</div>
<!-- Sign-in button -->
<button id="signInBtn" class="btn btn-primary" data-i18n="signIn.button">Sign In</button>
<button id="signInBtn" class="btn btn-primary" data-i18n="signIn.button">Join</button>
<!-- Direct call section -->
<div class="direct-call-section">
<div class="direct-call-divider">
<span data-i18n="signIn.directCallLabel">Or call someone directly</span>
</div>
<div class="direct-call-row">
<input
id="directCallInput"
type="text"
placeholder="Enter name to call"
data-i18n-placeholder="signIn.directCallPlaceholder"
autocomplete="off"
/>
<button id="directCallBtn" class="btn btn-call-now">
<i class="fas fa-phone"></i>
<span data-i18n="signIn.directCallButton">Call Now</span>
</button>
</div>
</div>
</div>
</div>
</div>
@@ -117,6 +142,42 @@
<!-- Session time -->
<span id="sessionTime">0s</span>
<!-- Calling overlay (outgoing) -->
<div id="callingOverlay" class="calling-overlay">
<div class="calling-overlay-content">
<div class="calling-pulse-ring"></div>
<i class="fas fa-phone calling-icon"></i>
<p class="calling-status" data-i18n="room.callingOverlay">Calling...</p>
<p id="callingUsername" class="calling-username"></p>
<span id="callingTimer" class="calling-timer">0s</span>
<button id="cancelCallBtn" class="btn btn-danger btn-cancel-call" data-i18n="room.cancelCall">
Cancel
</button>
</div>
</div>
<!-- Incoming call overlay -->
<div id="incomingCallOverlay" class="incoming-call-overlay">
<div class="incoming-call-content">
<div class="incoming-call-ring-outer"></div>
<div class="incoming-call-ring-inner"></div>
<div class="incoming-call-avatar">
<i class="fas fa-user"></i>
</div>
<p class="incoming-call-label" data-i18n="room.incomingCall">Incoming call</p>
<p id="incomingCallUsername" class="incoming-call-username"></p>
<div id="incomingCallTimer" class="incoming-call-timer"></div>
<div class="incoming-call-actions">
<button id="declineCallBtn" class="btn-call-action btn-decline" title="Decline">
<i class="fas fa-phone-slash"></i>
</button>
<button id="acceptCallBtn" class="btn-call-action btn-accept" title="Accept">
<i class="fas fa-phone"></i>
</button>
</div>
</div>
</div>
<!-- Remote video container (video from the other user) -->
<div id="remoteVideoContainer">
<!-- Local video container (your video) -->
+464 -2
View File
@@ -1812,7 +1812,7 @@ input {
/* Settings Section */
.settings-section {
padding: var(--spacing-lg);
padding: var(--spacing-lg) var(--spacing-xl);
background: rgba(15, 20, 25, 0.4);
height: 100%;
overflow-y: auto;
@@ -2070,7 +2070,7 @@ input {
}
.settings-section {
padding: 16px;
padding: var(--spacing-md) var(--spacing-lg);
}
.settings-title {
@@ -2484,4 +2484,466 @@ z-index:
- 5. userSidebar
- 6. file-transfer-status
- 7. SweetAlert2
- 8. callingOverlay
*/
/*--------------------------------------------------------------
# Sign-In Subtitle
--------------------------------------------------------------*/
.sign-in-subtitle {
margin: 8px 0 0 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--text-secondary);
opacity: 0.85;
}
/*--------------------------------------------------------------
# Direct Call Section (Sign-In Page)
--------------------------------------------------------------*/
.direct-call-section {
margin-top: var(--spacing-lg);
}
.direct-call-divider {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.direct-call-divider::before,
.direct-call-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--glass-border);
}
.direct-call-row {
display: flex;
gap: var(--spacing-sm);
}
.direct-call-row input {
flex: 1;
padding: 12px 16px;
font-size: 15px;
color: var(--text-color);
border: 2px solid var(--glass-border);
border-radius: var(--border-radius);
background: rgba(30, 35, 50, 0.5);
transition: all var(--transition-base);
font-weight: var(--font-weight-medium);
outline: none;
}
.direct-call-row input::placeholder {
color: var(--text-muted);
font-weight: var(--font-weight-normal);
}
.direct-call-row input:focus {
border-color: var(--primary-color);
background: rgba(30, 35, 50, 0.8);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
}
.btn-call-now {
background: linear-gradient(135deg, var(--success-color), var(--success-hover)) !important;
color: #fff !important;
border: none !important;
padding: 12px 20px !important;
border-radius: var(--border-radius) !important;
font-weight: var(--font-weight-semibold) !important;
cursor: pointer;
transition: all var(--transition-base) !important;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-call-now:hover {
background: linear-gradient(135deg, var(--success-hover), #047857) !important;
transform: translateY(-2px) !important;
box-shadow:
var(--shadow-md),
0 0 16px rgba(16, 185, 129, 0.4) !important;
}
/*--------------------------------------------------------------
# Calling Overlay
--------------------------------------------------------------*/
.calling-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 150;
background: rgba(15, 20, 25, 0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: none;
align-items: center;
justify-content: center;
border-radius: var(--border-radius-lg);
animation: fadeIn 0.3s ease-out;
}
.calling-overlay-content {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
}
.calling-pulse-ring {
width: 100px;
height: 100px;
border-radius: 50%;
border: 3px solid var(--primary-color);
animation: callingPulse 1.5s ease-out infinite;
position: absolute;
}
.calling-icon {
font-size: 2.5rem;
color: var(--primary-light);
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.15);
border-radius: 50%;
border: 2px solid var(--primary-color);
position: relative;
}
.calling-status {
color: var(--text-color);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
margin: 0;
}
.calling-username {
color: var(--primary-light);
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin: 0;
}
.calling-timer {
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.btn-cancel-call {
margin-top: var(--spacing-md);
padding: 10px 32px !important;
border-radius: 24px !important;
font-size: var(--font-size-base) !important;
}
@keyframes callingPulse {
0% {
transform: scale(1);
opacity: 0.8;
border-color: var(--primary-color);
}
100% {
transform: scale(1.8);
opacity: 0;
border-color: var(--primary-light);
}
}
/*--------------------------------------------------------------
# Incoming Call Overlay
--------------------------------------------------------------*/
.incoming-call-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 160;
background: rgba(10, 12, 18, 0.95);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
display: none;
align-items: center;
justify-content: center;
border-radius: var(--border-radius-lg);
animation: fadeIn 0.3s ease-out;
}
.incoming-call-content {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
position: relative;
}
.incoming-call-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(16, 185, 129, 0.2));
border: 2px solid rgba(59, 130, 246, 0.5);
display: flex;
align-items: center;
justify-content: center;
font-size: 2.2rem;
color: var(--primary-light);
position: relative;
z-index: 2;
}
.incoming-call-ring-outer,
.incoming-call-ring-inner {
position: absolute;
width: 90px;
height: 90px;
border-radius: 50%;
border: 2px solid var(--success-color);
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
.incoming-call-ring-outer {
animation: incomingRing 2s ease-out infinite;
}
.incoming-call-ring-inner {
animation: incomingRing 2s ease-out 0.5s infinite;
}
.incoming-call-label {
color: var(--text-muted);
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 2px;
margin: var(--spacing-md) 0 0 0;
}
.incoming-call-username {
color: var(--text-color);
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin: 0;
}
.incoming-call-timer {
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
margin: var(--spacing-sm) 0;
overflow: hidden;
position: relative;
}
.incoming-call-timer::after {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background: linear-gradient(90deg, var(--success-color), var(--primary-color));
animation: incomingTimerBar 10s linear forwards;
transform-origin: left;
}
.incoming-call-actions {
display: flex;
gap: 40px;
margin-top: var(--spacing-md);
}
.btn-call-action {
width: 64px;
height: 64px;
border-radius: 50%;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: #fff;
transition: all 0.2s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.btn-call-action:hover {
transform: scale(1.1);
}
.btn-call-action:active {
transform: scale(0.95);
}
.btn-decline {
background: linear-gradient(135deg, #ef4444, #dc2626);
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4);
}
.btn-decline:hover {
box-shadow: 0 6px 28px rgba(239, 68, 68, 0.5);
}
.btn-accept {
background: linear-gradient(135deg, #10b981, #059669);
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4);
animation: acceptPulse 1.5s ease-in-out infinite;
}
.btn-accept:hover {
box-shadow: 0 6px 28px rgba(16, 185, 129, 0.5);
animation: none;
}
@keyframes incomingRing {
0% {
transform: translateX(-50%) scale(1);
opacity: 0.6;
}
100% {
transform: translateX(-50%) scale(2);
opacity: 0;
}
}
@keyframes acceptPulse {
0%,
100% {
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4);
}
50% {
box-shadow: 0 4px 30px rgba(16, 185, 129, 0.7);
}
}
@keyframes incomingTimerBar {
0% {
transform: scaleX(1);
}
100% {
transform: scaleX(0);
}
}
/*--------------------------------------------------------------
# Empty States
--------------------------------------------------------------*/
.user-list-empty {
text-align: center;
padding: 40px 20px !important;
color: var(--text-muted);
cursor: default !important;
border-left: none !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.user-list-empty:hover {
background: none !important;
transform: none !important;
border-left-color: transparent !important;
}
.user-list-empty div {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
width: 100%;
}
.user-list-empty i {
font-size: 2rem;
margin-bottom: 12px;
display: block;
opacity: 0.5;
}
.user-list-empty p {
margin: 4px 0;
font-size: var(--font-size-sm);
width: 100%;
}
.user-list-empty p:last-child {
font-size: 0.85rem;
}
.chat-empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.chat-empty-state i {
font-size: 2rem;
margin-bottom: 12px;
display: block;
opacity: 0.5;
}
.chat-empty-state p {
margin: 4px 0;
font-size: var(--font-size-sm);
}
/*--------------------------------------------------------------
# Online User Count (Sign-In Page)
--------------------------------------------------------------*/
.online-count {
margin-top: var(--spacing-sm);
font-size: var(--font-size-xs);
color: var(--success-color);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
opacity: 0.9;
}
.online-count .online-dot {
width: 8px;
height: 8px;
background: var(--success-color);
border-radius: 50%;
display: inline-block;
animation: pulse 2s infinite;
}
@media (max-width: 768px) {
.direct-call-row {
flex-direction: column;
}
.direct-call-row .btn-call-now {
width: 100%;
justify-content: center;
}
}