From 70a7bd107e84a97f890f3bb29073aa67a039816e Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Mon, 6 Apr 2026 15:43:20 +0200 Subject: [PATCH] =?UTF-8?q?[call-me]=20-=20feat:=20UX=20overhaul=20?= =?UTF-8?q?=E2=80=94=20calling=20overlays,=20sign-in=20redesign,=20empty?= =?UTF-8?q?=20states,=20i18n=20updates,=20update=20dep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/locales/ar.json | 29 ++- app/locales/de.json | 29 ++- app/locales/en.json | 30 ++- app/locales/es.json | 29 ++- app/locales/fr.json | 29 ++- app/locales/hi.json | 31 ++- app/locales/hr.json | 29 ++- app/locales/it.json | 29 ++- app/locales/ja.json | 29 ++- app/locales/pt.json | 25 ++- app/locales/ru.json | 29 ++- app/locales/sr.json | 29 ++- app/locales/zh.json | 29 ++- app/server.js | 12 +- package-lock.json | 8 +- package.json | 2 +- public/assets/ring.png | Bin 9024 -> 0 bytes public/client.js | 256 +++++++++++++++++++--- public/index.html | 67 +++++- public/style.css | 466 ++++++++++++++++++++++++++++++++++++++++- 20 files changed, 1051 insertions(+), 136 deletions(-) delete mode 100644 public/assets/ring.png diff --git a/app/locales/ar.json b/app/locales/ar.json index de977c5..f39c952 100644 --- a/app/locales/ar.json +++ b/app/locales/ar.json @@ -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": "تم تغيير الميكروفون بنجاح", diff --git a/app/locales/de.json b/app/locales/de.json index 01ce47f..68fb334 100644 --- a/app/locales/de.json +++ b/app/locales/de.json @@ -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", diff --git a/app/locales/en.json b/app/locales/en.json index 1225362..f78ed75 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -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", diff --git a/app/locales/es.json b/app/locales/es.json index f872ee5..a9b2bac 100644 --- a/app/locales/es.json +++ b/app/locales/es.json @@ -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", diff --git a/app/locales/fr.json b/app/locales/fr.json index 573f818..a4ae3a4 100644 --- a/app/locales/fr.json +++ b/app/locales/fr.json @@ -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", diff --git a/app/locales/hi.json b/app/locales/hi.json index 3f5d6e3..129ffac 100644 --- a/app/locales/hi.json +++ b/app/locales/hi.json @@ -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": "माइक्रोफ़ोन सफलतापूर्वक बदला गया", diff --git a/app/locales/hr.json b/app/locales/hr.json index e156a0e..e407a75 100644 --- a/app/locales/hr.json +++ b/app/locales/hr.json @@ -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", diff --git a/app/locales/it.json b/app/locales/it.json index e509c90..3c75d70 100644 --- a/app/locales/it.json +++ b/app/locales/it.json @@ -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", diff --git a/app/locales/ja.json b/app/locales/ja.json index 3d591ca..6eb0fe2 100644 --- a/app/locales/ja.json +++ b/app/locales/ja.json @@ -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": "マイクを正常に変更しました", diff --git a/app/locales/pt.json b/app/locales/pt.json index c51914c..8591898 100644 --- a/app/locales/pt.json +++ b/app/locales/pt.json @@ -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", diff --git a/app/locales/ru.json b/app/locales/ru.json index f8265e1..4f36dca 100644 --- a/app/locales/ru.json +++ b/app/locales/ru.json @@ -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": "Микрофон успешно изменён", diff --git a/app/locales/sr.json b/app/locales/sr.json index fbbee44..27d6f3a 100644 --- a/app/locales/sr.json +++ b/app/locales/sr.json @@ -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", diff --git a/app/locales/zh.json b/app/locales/zh.json index b6213bc..b9ee038 100644 --- a/app/locales/zh.json +++ b/app/locales/zh.json @@ -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": "麦克风切换成功", diff --git a/app/server.js b/app/server.js index b9f3109..6b986b6 100755 --- a/app/server.js +++ b/app/server.js @@ -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}`); diff --git a/package-lock.json b/package-lock.json index e13640c..32a5739 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index df2b214..fa93d95 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/assets/ring.png b/public/assets/ring.png deleted file mode 100644 index e96446d92f83284c40ca832b29d7307b65d8059a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9024 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 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 = ` +
+ +

${t('room.noUsersOnline')}

+

${t('room.shareToInvite')}

+
+ `; + 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 = ` + +

${t('room.noChatMessages')}

+ `; + chatMessages.appendChild(emptyDiv); } // Device Management Functions diff --git a/public/index.html b/public/index.html index 43b2a88..4fe9248 100755 --- a/public/index.html +++ b/public/index.html @@ -68,7 +68,12 @@
-

Call-me

+
+

Call-me

+ +
@@ -76,7 +81,7 @@ @@ -105,7 +110,27 @@
- + + + +
+
+ Or call someone directly +
+
+ + +
+
@@ -117,6 +142,42 @@ 0s + +
+
+
+ +

Calling...

+

+ 0s + +
+
+ + +
+
+
+
+
+ +
+

Incoming call

+

+
+
+ + +
+
+
+
diff --git a/public/style.css b/public/style.css index f697e3c..acd4e98 100644 --- a/public/style.css +++ b/public/style.css @@ -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; + } +}