let socket;
let currentUser = null;
let currentToken = null;
const LANGUAGES = [
{ code: 'en', name: 'English', flag: '🇺🇸', country: 'United States' },
{ code: 'es', name: 'Español', flag: '🇪🇸', country: 'España' },
{ code: 'fr', name: 'Français', flag: '🇫🇷', country: 'France' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪', country: 'Deutschland' },
{ code: 'ja', name: '日本語', flag: '🇯🇵', country: 'Japan' },
{ code: 'pt', name: 'Português', flag: '🇧🇷', country: 'Brasil' },
{ code: 'ar', name: 'العربية', flag: '🇸🇦', country: 'السعودية' },
{ code: 'zh', name: '中文', flag: '🇨🇳', country: '中国' },
{ code: 'ru', name: 'Русский', flag: '🇷🇺', country: 'Россия' },
{ code: 'ko', name: '한국어', flag: '🇰🇷', country: '대한민국' },
{ code: 'tr', name: 'Türkçe', flag: '🇹🇷', country: 'Türkiye' },
{ code: 'it', name: 'Italiano', flag: '🇮🇹', country: 'Italia' },
];
const I18N = {
en: { login:'Login', register:'Register', send:'Send', settings:'Settings', friends:'Friends', profile:'Profile', logout:'Logout', search:'Search...', welcome:'Welcome back!', typeMsg:'Type a message...' },
es: { login:'Ingresar', register:'Registrarse', send:'Enviar', settings:'Ajustes', friends:'Amigos', profile:'Perfil', logout:'Cerrar sesión', search:'Buscar...', welcome:'¡Bienvenido!', typeMsg:'Escribe un mensaje...' },
fr: { login:'Connexion', register:"S'inscrire", send:'Envoyer', settings:'Paramètres', friends:'Amis', profile:'Profil', logout:'Déconnexion', search:'Rechercher...', welcome:'Content de vous revoir!', typeMsg:'Tapez un message...' },
de: { login:'Anmelden', register:'Registrieren', send:'Senden', settings:'Einstellungen', friends:'Freunde', profile:'Profil', logout:'Abmelden', search:'Suchen...', welcome:'Willkommen zurück!', typeMsg:'Nachricht eingeben...' },
ja: { login:'ログイン', register:'登録', send:'送信', settings:'設定', friends:'友達', profile:'プロフィール', logout:'ログアウト', search:'検索...', welcome:'おかえりなさい!', typeMsg:'メッセージを入力...' },
pt: { login:'Entrar', register:'Cadastrar', send:'Enviar', settings:'Configurações', friends:'Amigos', profile:'Perfil', logout:'Sair', search:'Pesquisar...', welcome:'Bem-vindo de volta!', typeMsg:'Digite uma mensagem...' },
ar: { login:'دخول', register:'تسجيل', send:'إرسال', settings:'الإعدادات', friends:'أصدقاء', profile:'الملف الشخصي', logout:'خروج', search:'بحث...', welcome:'مرحباً بعودتك!', typeMsg:'اكتب رسالة...' },
zh: { login:'登录', register:'注册', send:'发送', settings:'设置', friends:'朋友', profile:'个人资料', logout:'退出', search:'搜索...', welcome:'欢迎回来!', typeMsg:'输入消息...' },
ru: { login:'Войти', register:'Регистрация', send:'Отправить', settings:'Настройки', friends:'Друзья', profile:'Профиль', logout:'Выход', search:'Поиск...', welcome:'С возвращением!', typeMsg:'Введите сообщение...' },
ko: { login:'로그인', register:'회원가입', send:'전송', settings:'설정', friends:'친구', profile:'프로필', logout:'로그아웃', search:'검색...', welcome:'다시 오신 것을 환영해요!', typeMsg:'메시지 입력...' },
tr: { login:'Giriş', register:'Kayıt Ol', send:'Gönder', settings:'Ayarlar', friends:'Arkadaşlar', profile:'Profil', logout:'Çıkış', search:'Ara...', welcome:'Tekrar hoş geldiniz!', typeMsg:'Mesaj yaz...' },
it: { login:'Accedi', register:'Registrati', send:'Invia', settings:'Impostazioni', friends:'Amici', profile:'Profilo', logout:'Esci', search:'Cerca...', welcome:'Ben tornato!', typeMsg:'Scrivi un messaggio...' },
};
const UI_TRANSLATIONS = {
en: {
'Login': 'Login', 'Register': 'Register', 'Create Account': 'Create Account', 'Already have an account?': 'Already have an account?', "Don't have an account?": "Don't have an account?", 'Forgot password?': 'Forgot password?', 'Reset Password': 'Reset Password',
'🪪 Account': '🪪 Account', '🛡️ Privacy': '🛡️ Privacy', '🗨️ Chat': '🗨️ Chat', '📬 Notifications': '📬 Notifications', '🪄 Appearance': '🪄 Appearance', '🧭 Language': '🧭 Language', '📈 Statistics': '📈 Statistics',
'Settings': 'Settings', 'Message...': 'Message...', 'Send': 'Send', 'Close': 'Close', 'Communities': 'Communities', 'Groups': 'Groups', 'Open': 'Open', 'Join': 'Join', 'Manage': 'Manage', 'Create New Group': 'Create New Group',
'⚙️ Settings': '⚙️ Settings', '➕ Create New Group': '➕ Create New Group',
'Account Settings': 'Account Settings', 'Privacy & Safety': 'Privacy & Safety', 'Chat Settings': 'Chat Settings', 'Notifications': 'Notifications', 'Appearance': 'Appearance', '📊 Your Statistics': '📊 Your Statistics', '🌐 Language': '🌐 Language',
'Email': 'Email', 'Change Username': 'Change Username', 'Update Username': 'Update Username', 'Delete Account': 'Delete Account', 'Switch Account': 'Switch Account', 'Desktop notifications': 'Desktop notifications', 'Message sound': 'Message sound',
'Theme': 'Theme', 'Dark': 'Dark', 'Light': 'Light', 'Custom Colors': 'Custom Colors', 'Compact mode': 'Compact mode', 'Reset Statistics': 'Reset Statistics', '🎯 Events & Challenges': '🎯 Events & Challenges',
'App Language': 'App Language', 'Choose your preferred language. UI text will update immediately.': 'Choose your preferred language. UI text will update immediately.', 'Current:': 'Current:', 'All messages': 'All messages', 'Only mentions': 'Only mentions', 'Nothing': 'Nothing',
'Default Microphone': 'Default Microphone', 'Default Speaker': 'Default Speaker', 'Off': 'Off', '30 seconds': '30 seconds', '1 minute': '1 minute', '5 minutes': '5 minutes', '1 hour': '1 hour', 'Small': 'Small', 'Medium': 'Medium', 'Large': 'Large',
'Trivia Challenge': 'Trivia Challenge', 'Question': 'Question', 'Type your answer': 'Type your answer', 'Speed Typing': 'Speed Typing', 'Type this exactly': 'Type this exactly', 'Memory Challenge': 'Memory Challenge', 'Click emojis in order': 'Click emojis in order', 'Math Blitz': 'Math Blitz', 'Riddle Challenge': 'Riddle Challenge', 'Answer the riddle': 'Answer the riddle', 'Reaction Speed': 'Reaction Speed', 'Word Scramble': 'Word Scramble', 'Unscramble the word': 'Unscramble the word',
'Challenge cancelled': 'Challenge cancelled', 'coins earned': 'coins earned', 'correct': 'correct', 'Try again': 'Try again', 'Challenge complete': 'Challenge complete', 'Error loading': 'Error loading', 'Completed': 'Completed', 'Network error': 'Network error',
'Success': 'Success', 'Warning': 'Warning', 'Error': 'Error', 'Info': 'Info', 'Please login': 'Please login', 'Invalid input': 'Invalid input',
'Pending request': 'Pending request', 'Accept': 'Accept', 'Decline': 'Decline', 'No friends yet': 'No friends yet', 'Online': 'Online', 'Offline': 'Offline', 'Profile': 'Profile', 'No pending requests': 'No pending requests',
'Premium Shop': 'Premium Shop', 'Instant equip items, profile cosmetics, and seasonal packs': 'Instant equip items, profile cosmetics, and seasonal packs', 'Live Deals: up to 20% off featured items': 'Live Deals: up to 20% off featured items',
'No shop items available': 'No shop items available', 'Buy Now': 'Buy Now', 'Need Coins': 'Need Coins', 'Need': 'Need', 'You have': 'You have', 'Item purchased and applied!': 'Item purchased and applied!'
},
es: { 'Login':'Ingresar','Register':'Registrarse','Create Account':'Crear cuenta','Already have an account?':'¿Ya tienes una cuenta?','Don\'t have an account?':'¿No tienes una cuenta?','Forgot password?':'¿Olvidaste tu contraseña?','Reset Password':'Restablecer contraseña','🪪 Account':'🪪 Cuenta','🛡️ Privacy':'🛡️ Privacidad','🗨️ Chat':'🗨️ Chat','📬 Notifications':'📬 Notificaciones','🪄 Appearance':'🪄 Apariencia','🧭 Language':'🧭 Idioma','📈 Statistics':'📈 Estadísticas','⚙️ Settings':'⚙️ Ajustes','Message...':'Mensaje...','Send':'Enviar','Close':'Cerrar','Communities':'Comunidades','Groups':'Grupos','Open':'Abrir','Join':'Unirse','Manage':'Administrar','➕ Create New Group':'➕ Crear grupo','Account Settings':'Configuración de cuenta','Privacy & Safety':'Privacidad y seguridad','Chat Settings':'Configuración del chat','Notifications':'Notificaciones','Appearance':'Apariencia','📊 Your Statistics':'📊 Tus estadísticas','🌐 Language':'🌐 Idioma','Email':'Correo','Change Username':'Cambiar nombre','Update Username':'Actualizar nombre','Delete Account':'Eliminar cuenta','Switch Account':'Cambiar cuenta','Desktop notifications':'Notificaciones de escritorio','Message sound':'Sonido del mensaje','Theme':'Tema','Dark':'Oscuro','Light':'Claro','Custom Colors':'Colores personalizados','Compact mode':'Modo compacto','Reset Statistics':'Restablecer estadísticas','🎯 Events & Challenges':'🎯 Eventos y desafíos','App Language':'Idioma de la app','Choose your preferred language. UI text will update immediately.':'Elige tu idioma. La interfaz se actualiza al instante.','Current:':'Actual:','All messages':'Todos los mensajes','Only mentions':'Solo menciones','Nothing':'Nada','Default Microphone':'Micrófono predeterminado','Default Speaker':'Altavoz predeterminado','Off':'Apagado','30 seconds':'30 segundos','1 minute':'1 minuto','5 minutes':'5 minutos','1 hour':'1 hora','Small':'Pequeño','Medium':'Mediano','Large':'Grande','Trivia Challenge':'Desafío de trivia','Question':'Pregunta','Type your answer':'Escribe tu respuesta','Speed Typing':'Escritura rápida','Type this exactly':'Escribe esto exactamente','Memory Challenge':'Desafío de memoria','Click emojis in order':'Haz clic en emoji en orden','Math Blitz':'Blitz matemático','Riddle Challenge':'Desafío de adivinanzas','Answer the riddle':'Responde la adivinanza','Reaction Speed':'Velocidad de reacción','Word Scramble':'Palabras revueltas','Unscramble the word':'Descramble la palabra','Challenge cancelled':'Desafío cancelado','coins earned':'monedas ganadas','correct':'correcto','Try again':'Intenta de nuevo','Challenge complete':'Desafío completo','Error loading':'Error al cargar','Completed':'Completado','Network error':'Error de red','Success':'Éxito','Warning':'Advertencia','Error':'Error','Info':'Información','Please login':'Por favor inicia sesión','Invalid input':'Entrada inválida' },
fr: { 'Login':'Connexion','Register':'Inscription','Create Account':'Créer un compte','Already have an account?':'Vous avez déjà un compte ?','Don\'t have an account?':'Vous n\'avez pas de compte ?','Forgot password?':'Mot de passe oublié ?','Reset Password':'Réinitialiser le mot de passe','🪪 Account':'🪪 Compte','🛡️ Privacy':'🛡️ Confidentialité','🗨️ Chat':'🗨️ Chat','📬 Notifications':'📬 Notifications','🪄 Appearance':'🪄 Apparence','🧭 Language':'🧭 Langue','📈 Statistics':'📈 Statistiques','⚙️ Settings':'⚙️ Paramètres','Message...':'Message...','Send':'Envoyer','Close':'Fermer','Communities':'Communautés','Groups':'Groupes','Open':'Ouvrir','Join':'Rejoindre','Manage':'Gérer','➕ Create New Group':'➕ Créer un groupe','Account Settings':'Paramètres du compte','Privacy & Safety':'Confidentialité et sécurité','Chat Settings':'Paramètres du chat','Notifications':'Notifications','Appearance':'Apparence','📊 Your Statistics':'📊 Vos statistiques','🌐 Language':'🌐 Langue','Email':'E-mail','Change Username':'Changer le nom','Update Username':'Mettre à jour le nom','Delete Account':'Supprimer le compte','Switch Account':'Changer de compte','Desktop notifications':'Notifications du bureau','Message sound':'Son du message','Theme':'Thème','Dark':'Sombre','Light':'Clair','Custom Colors':'Couleurs personnalisées','Compact mode':'Mode compact','Reset Statistics':'Réinitialiser les statistiques','🎯 Events & Challenges':'🎯 Événements et défis','App Language':'Langue de l\'application','Choose your preferred language. UI text will update immediately.':'Choisissez votre langue. L\'interface se met à jour immédiatement.','Current:':'Actuel :','All messages':'Tous les messages','Only mentions':'Mentions uniquement','Nothing':'Rien','Default Microphone':'Micro par défaut','Default Speaker':'Haut-parleur par défaut','Off':'Désactivé','30 seconds':'30 secondes','1 minute':'1 minute','5 minutes':'5 minutes','1 hour':'1 heure','Small':'Petit','Medium':'Moyen','Large':'Grand' },
de: { 'Login':'Anmelden','Register':'Registrieren','Create Account':'Konto erstellen','Already have an account?':'Hast du bereits ein Konto?','Don\'t have an account?':'Noch kein Konto?','Forgot password?':'Passwort vergessen?','Reset Password':'Passwort zurücksetzen','🪪 Account':'🪪 Konto','🛡️ Privacy':'🛡️ Datenschutz','🗨️ Chat':'🗨️ Chat','📬 Notifications':'📬 Benachrichtigungen','🪄 Appearance':'🪄 Darstellung','🧭 Language':'🧭 Sprache','📈 Statistics':'📈 Statistiken','⚙️ Settings':'⚙️ Einstellungen','Message...':'Nachricht...','Send':'Senden','Close':'Schließen','Communities':'Communitys','Groups':'Gruppen','Open':'Öffnen','Join':'Beitreten','Manage':'Verwalten','➕ Create New Group':'➕ Neue Gruppe erstellen','Account Settings':'Kontoeinstellungen','Privacy & Safety':'Datenschutz und Sicherheit','Chat Settings':'Chat-Einstellungen','Notifications':'Benachrichtigungen','Appearance':'Darstellung','📊 Your Statistics':'📊 Deine Statistiken','🌐 Language':'🌐 Sprache','Email':'E-Mail','Change Username':'Benutzernamen ändern','Update Username':'Namen aktualisieren','Delete Account':'Konto löschen','Switch Account':'Konto wechseln','Desktop notifications':'Desktop-Benachrichtigungen','Message sound':'Nachrichtenton','Theme':'Thema','Dark':'Dunkel','Light':'Hell','Custom Colors':'Eigene Farben','Compact mode':'Kompaktmodus','Reset Statistics':'Statistiken zurücksetzen','🎯 Events & Challenges':'🎯 Events und Challenges','App Language':'App-Sprache','Choose your preferred language. UI text will update immediately.':'Wähle deine Sprache. Die Oberfläche wird sofort aktualisiert.','Current:':'Aktuell:','All messages':'Alle Nachrichten','Only mentions':'Nur Erwähnungen','Nothing':'Nichts','Default Microphone':'Standardmikrofon','Default Speaker':'Standardlautsprecher','Off':'Aus','30 seconds':'30 Sekunden','1 minute':'1 Minute','5 minutes':'5 Minuten','1 hour':'1 Stunde','Small':'Klein','Medium':'Mittel','Large':'Groß' },
ja: { 'Login':'ログイン','Register':'登録','Create Account':'アカウント作成','Already have an account?':'すでにアカウントがありますか?','Don\'t have an account?':'アカウントをお持ちではありませんか?','Forgot password?':'パスワードを忘れましたか?','Reset Password':'パスワードをリセット','🪪 Account':'🪪 アカウント','🛡️ Privacy':'🛡️ プライバシー','🗨️ Chat':'🗨️ チャット','📬 Notifications':'📬 通知','🪄 Appearance':'🪄 外観','🧭 Language':'🧭 言語','📈 Statistics':'📈 統計','⚙️ Settings':'⚙️ 設定','Message...':'メッセージ...','Send':'送信','Close':'閉じる','Communities':'コミュニティ','Groups':'グループ','Open':'開く','Join':'参加','Manage':'管理','➕ Create New Group':'➕ 新しいグループを作成','Account Settings':'アカウント設定','Privacy & Safety':'プライバシーと安全','Chat Settings':'チャット設定','Notifications':'通知','Appearance':'外観','📊 Your Statistics':'📊 あなたの統計','🌐 Language':'🌐 言語','Email':'メール','Change Username':'ユーザー名を変更','Update Username':'ユーザー名を更新','Delete Account':'アカウント削除','Switch Account':'アカウント切替','Desktop notifications':'デスクトップ通知','Message sound':'メッセージ音','Theme':'テーマ','Dark':'ダーク','Light':'ライト','Custom Colors':'カスタムカラー','Compact mode':'コンパクトモード','Reset Statistics':'統計をリセット','🎯 Events & Challenges':'🎯 イベントとチャレンジ','App Language':'アプリ言語','Choose your preferred language. UI text will update immediately.':'言語を選択すると、UIがすぐに更新されます。','Current:':'現在:','All messages':'すべてのメッセージ','Only mentions':'メンションのみ','Nothing':'なし','Default Microphone':'既定のマイク','Default Speaker':'既定のスピーカー','Off':'オフ','30 seconds':'30秒','1 minute':'1分','5 minutes':'5分','1 hour':'1時間','Small':'小','Medium':'中','Large':'大' },
pt: { 'Login':'Entrar','Register':'Registrar','Create Account':'Criar conta','Already have an account?':'Já tem uma conta?','Don\'t have an account?':'Não tem uma conta?','Forgot password?':'Esqueceu a senha?','Reset Password':'Redefinir senha','🪪 Account':'🪪 Conta','🛡️ Privacy':'🛡️ Privacidade','🗨️ Chat':'🗨️ Chat','📬 Notifications':'📬 Notificações','🪄 Appearance':'🪄 Aparência','🧭 Language':'🧭 Idioma','📈 Statistics':'📈 Estatísticas','⚙️ Settings':'⚙️ Configurações','Message...':'Mensagem...','Send':'Enviar','Close':'Fechar','Communities':'Comunidades','Groups':'Grupos','Open':'Abrir','Join':'Entrar','Manage':'Gerenciar','➕ Create New Group':'➕ Criar novo grupo','Account Settings':'Configurações da conta','Privacy & Safety':'Privacidade e segurança','Chat Settings':'Configurações do chat','Notifications':'Notificações','Appearance':'Aparência','📊 Your Statistics':'📊 Suas estatísticas','🌐 Language':'🌐 Idioma','Email':'E-mail','Change Username':'Alterar nome','Update Username':'Atualizar nome','Delete Account':'Excluir conta','Switch Account':'Trocar conta','Desktop notifications':'Notificações da área de trabalho','Message sound':'Som da mensagem','Theme':'Tema','Dark':'Escuro','Light':'Claro','Custom Colors':'Cores personalizadas','Compact mode':'Modo compacto','Reset Statistics':'Redefinir estatísticas','🎯 Events & Challenges':'🎯 Eventos e desafios','App Language':'Idioma do app','Choose your preferred language. UI text will update immediately.':'Escolha seu idioma. A interface muda imediatamente.','Current:':'Atual:','All messages':'Todas as mensagens','Only mentions':'Só menções','Nothing':'Nada','Default Microphone':'Microfone padrão','Default Speaker':'Alto-falante padrão','Off':'Desligado','30 seconds':'30 segundos','1 minute':'1 minuto','5 minutes':'5 minutos','1 hour':'1 hora','Small':'Pequeno','Medium':'Médio','Large':'Grande' },
ar: { 'Login':'دخول','Register':'تسجيل','Create Account':'إنشاء حساب','Already have an account?':'هل لديك حساب بالفعل؟','Don\'t have an account?':'ليس لديك حساب؟','Forgot password?':'هل نسيت كلمة المرور؟','Reset Password':'إعادة تعيين كلمة المرور','🪪 Account':'🪪 الحساب','🛡️ Privacy':'🛡️ الخصوصية','🗨️ Chat':'🗨️ الدردشة','📬 Notifications':'📬 الإشعارات','🪄 Appearance':'🪄 المظهر','🧭 Language':'🧭 اللغة','📈 Statistics':'📈 الإحصائيات','⚙️ Settings':'⚙️ الإعدادات','Message...':'رسالة...','Send':'إرسال','Close':'إغلاق','Communities':'المجتمعات','Groups':'المجموعات','Open':'فتح','Join':'انضمام','Manage':'إدارة','➕ Create New Group':'➕ إنشاء مجموعة','Account Settings':'إعدادات الحساب','Privacy & Safety':'الخصوصية والأمان','Chat Settings':'إعدادات الدردشة','Notifications':'الإشعارات','Appearance':'المظهر','📊 Your Statistics':'📊 إحصاءاتك','🌐 Language':'🌐 اللغة','Email':'البريد الإلكتروني','Change Username':'تغيير الاسم','Update Username':'تحديث الاسم','Delete Account':'حذف الحساب','Switch Account':'تبديل الحساب','Desktop notifications':'إشعارات سطح المكتب','Message sound':'صوت الرسالة','Theme':'السمة','Dark':'داكن','Light':'فاتح','Custom Colors':'ألوان مخصصة','Compact mode':'الوضع المضغوط','Reset Statistics':'إعادة تعيين الإحصائيات','🎯 Events & Challenges':'🎯 الفعاليات والتحديات','App Language':'لغة التطبيق','Choose your preferred language. UI text will update immediately.':'اختر لغتك المفضلة. سيتم تحديث الواجهة فوراً.','Current:':'الحالي:','All messages':'كل الرسائل','Only mentions':'الإشارات فقط','Nothing':'لا شيء','Default Microphone':'الميكروفون الافتراضي','Default Speaker':'مكبر الصوت الافتراضي','Off':'إيقاف','30 seconds':'30 ثانية','1 minute':'دقيقة واحدة','5 minutes':'5 دقائق','1 hour':'ساعة واحدة','Small':'صغير','Medium':'متوسط','Large':'كبير' },
zh: { 'Login':'登录','Register':'注册','Create Account':'创建账户','Already have an account?':'已经有账户了?','Don\'t have an account?':'还没有账户?','Forgot password?':'忘记密码?','Reset Password':'重置密码','🪪 Account':'🪪 账户','🛡️ Privacy':'🛡️ 隐私','🗨️ Chat':'🗨️ 聊天','📬 Notifications':'📬 通知','🪄 Appearance':'🪄 外观','🧭 Language':'🧭 语言','📈 Statistics':'📈 统计','⚙️ Settings':'⚙️ 设置','Message...':'消息...','Send':'发送','Close':'关闭','Communities':'社区','Groups':'群组','Open':'打开','Join':'加入','Manage':'管理','➕ Create New Group':'➕ 创建新群组','Account Settings':'账户设置','Privacy & Safety':'隐私与安全','Chat Settings':'聊天设置','Notifications':'通知','Appearance':'外观','📊 Your Statistics':'📊 你的统计','🌐 Language':'🌐 语言','Email':'邮箱','Change Username':'修改用户名','Update Username':'更新用户名','Delete Account':'删除账户','Switch Account':'切换账户','Desktop notifications':'桌面通知','Message sound':'消息提示音','Theme':'主题','Dark':'深色','Light':'浅色','Custom Colors':'自定义颜色','Compact mode':'紧凑模式','Reset Statistics':'重置统计','🎯 Events & Challenges':'🎯 活动与挑战','App Language':'应用语言','Choose your preferred language. UI text will update immediately.':'选择你的语言,界面会立即更新。','Current:':'当前:','All messages':'所有消息','Only mentions':'仅提及','Nothing':'无','Default Microphone':'默认麦克风','Default Speaker':'默认扬声器','Off':'关闭','30 seconds':'30秒','1 minute':'1分钟','5 minutes':'5分钟','1 hour':'1小时','Small':'小','Medium':'中','Large':'大' },
ru: { 'Login':'Войти','Register':'Регистрация','Create Account':'Создать аккаунт','Already have an account?':'Уже есть аккаунт?','Don\'t have an account?':'Нет аккаунта?','Forgot password?':'Забыли пароль?','Reset Password':'Сбросить пароль','🪪 Account':'🪪 Аккаунт','🛡️ Privacy':'🛡️ Конфиденциальность','🗨️ Chat':'🗨️ Чат','📬 Notifications':'📬 Уведомления','🪄 Appearance':'🪄 Внешний вид','🧭 Language':'🧭 Язык','📈 Statistics':'📈 Статистика','⚙️ Settings':'⚙️ Настройки','Message...':'Сообщение...','Send':'Отправить','Close':'Закрыть','Communities':'Сообщества','Groups':'Группы','Open':'Открыть','Join':'Вступить','Manage':'Управлять','➕ Create New Group':'➕ Создать группу','Account Settings':'Настройки аккаунта','Privacy & Safety':'Конфиденциальность и безопасность','Chat Settings':'Настройки чата','Notifications':'Уведомления','Appearance':'Внешний вид','📊 Your Statistics':'📊 Ваша статистика','🌐 Language':'🌐 Язык','Email':'Эл. почта','Change Username':'Изменить имя','Update Username':'Обновить имя','Delete Account':'Удалить аккаунт','Switch Account':'Сменить аккаунт','Desktop notifications':'Уведомления на рабочем столе','Message sound':'Звук сообщения','Theme':'Тема','Dark':'Тёмная','Light':'Светлая','Custom Colors':'Свои цвета','Compact mode':'Компактный режим','Reset Statistics':'Сбросить статистику','🎯 Events & Challenges':'🎯 События и испытания','App Language':'Язык приложения','Choose your preferred language. UI text will update immediately.':'Выберите язык. Интерфейс обновится сразу.','Current:':'Текущий:','All messages':'Все сообщения','Only mentions':'Только упоминания','Nothing':'Ничего','Default Microphone':'Микрофон по умолчанию','Default Speaker':'Динамик по умолчанию','Off':'Выкл','30 seconds':'30 секунд','1 minute':'1 минута','5 minutes':'5 минут','1 hour':'1 час','Small':'Маленький','Medium':'Средний','Large':'Большой' },
ko: { 'Login':'로그인','Register':'회원가입','Create Account':'계정 만들기','Already have an account?':'이미 계정이 있나요?','Don\'t have an account?':'계정이 없나요?','Forgot password?':'비밀번호를 잊으셨나요?','Reset Password':'비밀번호 재설정','🪪 Account':'🪪 계정','🛡️ Privacy':'🛡️ 개인정보','🗨️ Chat':'🗨️ 채팅','📬 Notifications':'📬 알림','🪄 Appearance':'🪄 화면','🧭 Language':'🧭 언어','📈 Statistics':'📈 통계','⚙️ Settings':'⚙️ 설정','Message...':'메시지...','Send':'보내기','Close':'닫기','Communities':'커뮤니티','Groups':'그룹','Open':'열기','Join':'참여','Manage':'관리','➕ Create New Group':'➕ 새 그룹 만들기','Account Settings':'계정 설정','Privacy & Safety':'개인정보 및 보안','Chat Settings':'채팅 설정','Notifications':'알림','Appearance':'화면','📊 Your Statistics':'📊 내 통계','🌐 Language':'🌐 언어','Email':'이메일','Change Username':'이름 변경','Update Username':'이름 업데이트','Delete Account':'계정 삭제','Switch Account':'계정 전환','Desktop notifications':'데스크톱 알림','Message sound':'메시지 소리','Theme':'테마','Dark':'다크','Light':'라이트','Custom Colors':'사용자 색상','Compact mode':'컴팩트 모드','Reset Statistics':'통계 초기화','🎯 Events & Challenges':'🎯 이벤트 및 챌린지','App Language':'앱 언어','Choose your preferred language. UI text will update immediately.':'원하는 언어를 선택하면 UI가 즉시 바뀝니다.','Current:':'현재:','All messages':'모든 메시지','Only mentions':'멘션만','Nothing':'없음','Default Microphone':'기본 마이크','Default Speaker':'기본 스피커','Off':'끔','30 seconds':'30초','1 minute':'1분','5 minutes':'5분','1 hour':'1시간','Small':'작게','Medium':'보통','Large':'크게' },
tr: { 'Login':'Giriş','Register':'Kayıt Ol','Create Account':'Hesap oluştur','Already have an account?':'Zaten hesabın var mı?','Don\'t have an account?':'Hesabın yok mu?','Forgot password?':'Şifreni mi unuttun?','Reset Password':'Şifreyi sıfırla','🪪 Account':'🪪 Hesap','🛡️ Privacy':'🛡️ Gizlilik','🗨️ Chat':'🗨️ Sohbet','📬 Notifications':'📬 Bildirimler','🪄 Appearance':'🪄 Görünüm','🧭 Language':'🧭 Dil','📈 Statistics':'📈 İstatistikler','⚙️ Settings':'⚙️ Ayarlar','Message...':'Mesaj...','Send':'Gönder','Close':'Kapat','Communities':'Topluluklar','Groups':'Gruplar','Open':'Aç','Join':'Katıl','Manage':'Yönet','➕ Create New Group':'➕ Yeni grup oluştur','Account Settings':'Hesap ayarları','Privacy & Safety':'Gizlilik ve güvenlik','Chat Settings':'Sohbet ayarları','Notifications':'Bildirimler','Appearance':'Görünüm','📊 Your Statistics':'📊 İstatistiklerin','🌐 Language':'🌐 Dil','Email':'E-posta','Change Username':'Kullanıcı adını değiştir','Update Username':'Adı güncelle','Delete Account':'Hesabı sil','Switch Account':'Hesap değiştir','Desktop notifications':'Masaüstü bildirimleri','Message sound':'Mesaj sesi','Theme':'Tema','Dark':'Koyu','Light':'Açık','Custom Colors':'Özel renkler','Compact mode':'Kompakt mod','Reset Statistics':'İstatistikleri sıfırla','🎯 Events & Challenges':'🎯 Etkinlikler ve görevler','App Language':'Uygulama dili','Choose your preferred language. UI text will update immediately.':'Tercih ettiğin dili seç. Arayüz hemen güncellenir.','Current:':'Geçerli:','All messages':'Tüm mesajlar','Only mentions':'Sadece bahsetmeler','Nothing':'Hiçbiri','Default Microphone':'Varsayılan mikrofon','Default Speaker':'Varsayılan hoparlör','Off':'Kapalı','30 seconds':'30 saniye','1 minute':'1 dakika','5 minutes':'5 dakika','1 hour':'1 saat','Small':'Küçük','Medium':'Orta','Large':'Büyük' },
it: { 'Login':'Accedi','Register':'Registrati','Create Account':'Crea account','Already have an account?':'Hai già un account?','Don\'t have an account?':'Non hai un account?','Forgot password?':'Password dimenticata?','Reset Password':'Reimposta password','🪪 Account':'🪪 Account','🛡️ Privacy':'🛡️ Privacy','🗨️ Chat':'🗨️ Chat','📬 Notifications':'📬 Notifiche','🪄 Appearance':'🪄 Aspetto','🧭 Language':'🧭 Lingua','📈 Statistics':'📈 Statistiche','⚙️ Settings':'⚙️ Impostazioni','Message...':'Messaggio...','Send':'Invia','Close':'Chiudi','Communities':'Comunità','Groups':'Gruppi','Open':'Apri','Join':'Unisciti','Manage':'Gestisci','➕ Create New Group':'➕ Crea nuovo gruppo','Account Settings':'Impostazioni account','Privacy & Safety':'Privacy e sicurezza','Chat Settings':'Impostazioni chat','Notifications':'Notifiche','Appearance':'Aspetto','📊 Your Statistics':'📊 Le tue statistiche','🌐 Language':'🌐 Lingua','Email':'Email','Change Username':'Cambia nome utente','Update Username':'Aggiorna nome utente','Delete Account':'Elimina account','Switch Account':'Cambia account','Desktop notifications':'Notifiche desktop','Message sound':'Suono messaggio','Theme':'Tema','Dark':'Scuro','Light':'Chiaro','Custom Colors':'Colori personalizzati','Compact mode':'Modalità compatta','Reset Statistics':'Reimposta statistiche','🎯 Events & Challenges':'🎯 Eventi e sfide','App Language':'Lingua app','Choose your preferred language. UI text will update immediately.':'Scegli la lingua preferita. L\'interfaccia si aggiorna subito.','Current:':'Attuale:','All messages':'Tutti i messaggi','Only mentions':'Solo menzioni','Nothing':'Niente','Default Microphone':'Microfono predefinito','Default Speaker':'Altoparlante predefinito','Off':'Disattivato','30 seconds':'30 secondi','1 minute':'1 minuto','5 minutes':'5 minuti','1 hour':'1 ora','Small':'Piccolo','Medium':'Medio','Large':'Grande' }
};
const FIRETECH_BOT_USER = {
userId: 'bot_firetech_0',
id: 'bot_firetech_0',
username: '⚡ FireTech Bot',
role: 'Security Assistant',
isBot: true,
avatar: null
};
const FIRETECH_SECURITY_QUOTE = 'Secure your conversations. Trust the fire.';
const preferredLanguage = localStorage.getItem('appLanguage') || 'en';
let currentLanguage = 'en';
function getUiDictionary(code) {
return UI_TRANSLATIONS[code] || UI_TRANSLATIONS.en;
}
const reverseUiDictionaryCache = {};
function getReverseUiDictionary(code) {
if (!code || code === 'en') return {};
if (reverseUiDictionaryCache[code]) return reverseUiDictionaryCache[code];
const dict = getUiDictionary(code);
const reverse = {};
Object.entries(dict).forEach(([english, translated]) => {
const key = String(translated || '').trim().toLowerCase();
if (!key) return;
if (!reverse[key]) reverse[key] = english;
});
reverseUiDictionaryCache[code] = reverse;
return reverse;
}
function getSourceSeedText(rawText, langCode) {
const raw = String(rawText || '');
const trimmed = raw.trim();
if (!trimmed || !langCode || langCode === 'en') return raw;
const reverse = getReverseUiDictionary(langCode);
const direct = reverse[trimmed.toLowerCase()];
if (direct) return direct;
const emojiSplit = splitLeadingEmojiPrefix(trimmed);
const rest = String(emojiSplit.rest || '').trim();
const mappedRest = reverse[rest.toLowerCase()];
if (mappedRest) return `${emojiSplit.prefix}${mappedRest}`;
return raw;
}
const AUTO_TRANSLATION_CACHE_KEY = 'autoTranslationCache_v1';
let autoTranslationCache = {};
const i18nTextNodeSource = new WeakMap();
try {
autoTranslationCache = JSON.parse(localStorage.getItem(AUTO_TRANSLATION_CACHE_KEY) || '{}');
} catch (_) {
autoTranslationCache = {};
}
const autoTranslationInFlight = new Map();
const AUTO_TRANSLATION_ENABLED = localStorage.getItem('autoTranslateEnabled') !== 'false';
const AUTO_TRANSLATION_MIN_INTERVAL_MS = 350;
const AUTO_TRANSLATION_RATE_LIMIT_COOLDOWN_MS = 10 * 60 * 1000;
const AUTO_TRANSLATION_FAILURE_COOLDOWN_MS = 30 * 60 * 1000;
const AUTO_TRANSLATION_MAX_REQUESTS_PER_PAGE = 120;
let autoTranslationLastRequestAt = 0;
let autoTranslationRateLimitedUntil = 0;
const autoTranslationFailureUntil = new Map();
let autoTranslationRequestCount = 0;
function getAutoTranslationCacheKey(text, langCode) {
return `${langCode}::${String(text || '').trim()}`;
}
function getCachedAutoTranslation(text, langCode) {
return autoTranslationCache[getAutoTranslationCacheKey(text, langCode)] || null;
}
function setCachedAutoTranslation(text, langCode, translated) {
autoTranslationCache[getAutoTranslationCacheKey(text, langCode)] = translated;
try {
localStorage.setItem(AUTO_TRANSLATION_CACHE_KEY, JSON.stringify(autoTranslationCache));
} catch (_) {
}
}
function shouldAutoTranslate(raw) {
const text = String(raw || '').trim();
if (!text) return false;
if (text.length > 180) return false;
if (/^\d+[\d\s.,:/-]*$/.test(text)) return false;
if (/^[^\p{L}]+$/u.test(text)) return false;
return true;
}
async function fetchAutoTranslation(text, langCode) {
if (!AUTO_TRANSLATION_ENABLED) return null;
const key = getAutoTranslationCacheKey(text, langCode);
if (autoTranslationInFlight.has(key)) return autoTranslationInFlight.get(key);
const now = Date.now();
const blockedUntil = autoTranslationFailureUntil.get(key) || 0;
if (blockedUntil > now) return null;
if (autoTranslationRateLimitedUntil > now) return null;
if (autoTranslationRequestCount >= AUTO_TRANSLATION_MAX_REQUESTS_PER_PAGE) return null;
const request = (async () => {
try {
autoTranslationRequestCount += 1;
const waitMs = Math.max(0, AUTO_TRANSLATION_MIN_INTERVAL_MS - (Date.now() - autoTranslationLastRequestAt));
if (waitMs > 0) {
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
autoTranslationLastRequestAt = Date.now();
const source = encodeURIComponent(String(text));
const pair = encodeURIComponent(`en|${langCode}`);
const response = await fetch(`https://api.mymemory.translated.net/get?q=${source}&langpair=${pair}`);
if (response.status === 429) {
autoTranslationRateLimitedUntil = Date.now() + AUTO_TRANSLATION_RATE_LIMIT_COOLDOWN_MS;
autoTranslationFailureUntil.set(key, Date.now() + AUTO_TRANSLATION_FAILURE_COOLDOWN_MS);
return null;
}
if (!response.ok) return null;
const data = await response.json();
const translated = String(data?.responseData?.translatedText || '').trim();
if (!translated || translated.toLowerCase() === String(text).trim().toLowerCase()) return null;
setCachedAutoTranslation(text, langCode, translated);
autoTranslationFailureUntil.delete(key);
return translated;
} catch (_) {
autoTranslationFailureUntil.set(key, Date.now() + AUTO_TRANSLATION_FAILURE_COOLDOWN_MS);
return null;
} finally {
autoTranslationInFlight.delete(key);
}
})();
autoTranslationInFlight.set(key, request);
return request;
}
function applyAutoTranslateToElement(element, attr, originalText, langCode, dict) {
if (!AUTO_TRANSLATION_ENABLED) return;
if (langCode === 'en') return;
const mapped = dict[String(originalText || '').trim()];
if (mapped) return;
if (!shouldAutoTranslate(originalText)) return;
const cached = getCachedAutoTranslation(originalText, langCode);
if (cached) {
if (attr === 'text') {
if (element.textContent !== cached) element.textContent = cached;
} else if (element.getAttribute(attr) !== cached) {
element.setAttribute(attr, cached);
}
return;
}
fetchAutoTranslation(originalText, langCode).then((translated) => {
if (!translated) return;
if (langCode !== currentLanguage) return;
if (attr === 'text') {
if (element.textContent !== translated) element.textContent = translated;
} else if (element.getAttribute(attr) !== translated) {
element.setAttribute(attr, translated);
}
});
}
let isApplyingLocalization = false;
function splitLeadingEmojiPrefix(value) {
const raw = String(value || '');
const match = raw.match(/^([\p{Emoji}\p{Emoji_Component}\uFE0F\s]+)([\s\S]+)$/u);
if (!match) {
return { prefix: '', rest: raw };
}
return {
prefix: match[1] || '',
rest: match[2] || ''
};
}
function translateRawText(value, dict) {
const raw = String(value || '');
const trimmed = raw.trim();
if (!trimmed) return value;
if (dict[trimmed]) return dict[trimmed];
const directCaseInsensitiveKey = Object.keys(dict).find((key) => key.toLowerCase() === trimmed.toLowerCase());
if (directCaseInsensitiveKey) return dict[directCaseInsensitiveKey];
const emojiSplit = splitLeadingEmojiPrefix(trimmed);
const rest = String(emojiSplit.rest || '').trim();
if (rest) {
if (dict[rest]) return `${emojiSplit.prefix}${dict[rest]}`;
const restCaseInsensitiveKey = Object.keys(dict).find((key) => key.toLowerCase() === rest.toLowerCase());
if (restCaseInsensitiveKey) return `${emojiSplit.prefix}${dict[restCaseInsensitiveKey]}`;
}
return value;
}
function resolveLocalizedValue(originalText, dict, langCode) {
const original = String(originalText || '');
const dictTranslated = translateRawText(original, dict);
if (dictTranslated !== original) {
return { value: dictTranslated, shouldFetch: false };
}
if (AUTO_TRANSLATION_ENABLED && langCode !== 'en' && shouldAutoTranslate(original)) {
const cached = getCachedAutoTranslation(original, langCode);
if (cached) {
return { value: cached, shouldFetch: false };
}
return { value: original, shouldFetch: true };
}
return { value: original, shouldFetch: false };
}
function localizeContainer(container, dict) {
if (!container) return;
const langCode = currentLanguage;
container.querySelectorAll('*').forEach((element) => {
if (['SCRIPT', 'STYLE'].includes(element.tagName)) return;
if (element.childNodes.length > 0) {
element.childNodes.forEach((node) => {
if (node.nodeType !== Node.TEXT_NODE) return;
const rawCurrent = String(node.textContent || '');
if (!rawCurrent.trim()) return;
if (!i18nTextNodeSource.has(node)) {
i18nTextNodeSource.set(node, getSourceSeedText(rawCurrent, langCode));
}
const sourceText = String(i18nTextNodeSource.get(node) || '');
const match = sourceText.match(/^(\s*)([\s\S]*?)(\s*)$/);
const leading = match ? match[1] : '';
const core = (match ? match[2] : sourceText).trim();
const trailing = match ? match[3] : '';
if (!core) return;
const resolved = resolveLocalizedValue(core, dict, langCode);
const nextText = `${leading}${resolved.value}${trailing}`;
if (node.textContent !== nextText) {
node.textContent = nextText;
}
if (resolved.shouldFetch) {
fetchAutoTranslation(core, langCode).then((translated) => {
if (!translated) return;
if (langCode !== currentLanguage) return;
if (!node.isConnected) return;
const translatedText = `${leading}${translated}${trailing}`;
if (node.textContent !== translatedText) {
node.textContent = translatedText;
}
});
}
});
}
if (element.matches('input[placeholder], textarea[placeholder]')) {
if (!element.dataset.i18nPlaceholder) {
element.dataset.i18nPlaceholder = getSourceSeedText(element.getAttribute('placeholder') || '', langCode);
}
const resolvedPlaceholder = resolveLocalizedValue(element.dataset.i18nPlaceholder, dict, langCode);
if (element.getAttribute('placeholder') !== resolvedPlaceholder.value) {
element.setAttribute('placeholder', resolvedPlaceholder.value);
}
if (resolvedPlaceholder.shouldFetch) {
applyAutoTranslateToElement(element, 'placeholder', element.dataset.i18nPlaceholder, langCode, dict);
}
}
if (element.tagName === 'OPTION') {
if (!element.dataset.i18nText) element.dataset.i18nText = getSourceSeedText(element.textContent.trim(), langCode);
const resolvedOption = resolveLocalizedValue(element.dataset.i18nText, dict, langCode);
if (element.textContent !== resolvedOption.value) {
element.textContent = resolvedOption.value;
}
if (resolvedOption.shouldFetch) {
applyAutoTranslateToElement(element, 'text', element.dataset.i18nText, langCode, dict);
}
}
if (element.hasAttribute('title')) {
if (!element.dataset.i18nTitle) {
element.dataset.i18nTitle = getSourceSeedText(element.getAttribute('title') || '', langCode);
}
const resolvedTitle = resolveLocalizedValue(element.dataset.i18nTitle, dict, langCode);
if (element.getAttribute('title') !== resolvedTitle.value) {
element.setAttribute('title', resolvedTitle.value);
}
if (resolvedTitle.shouldFetch) {
applyAutoTranslateToElement(element, 'title', element.dataset.i18nTitle, langCode, dict);
}
}
if (element.children.length === 0) {
const raw = element.textContent.trim();
if (!raw) return;
if (!element.dataset.i18nText) element.dataset.i18nText = getSourceSeedText(raw, langCode);
const resolvedText = resolveLocalizedValue(element.dataset.i18nText, dict, langCode);
if (element.textContent !== resolvedText.value) {
element.textContent = resolvedText.value;
}
if (resolvedText.shouldFetch) {
applyAutoTranslateToElement(element, 'text', element.dataset.i18nText, langCode, dict);
}
}
});
}
function localizeStaticUi(code) {
const dict = getUiDictionary(code);
if (isApplyingLocalization) return;
isApplyingLocalization = true;
try {
localizeContainer(document.body, dict);
document.body.dir = code === 'ar' ? 'rtl' : 'ltr';
} finally {
isApplyingLocalization = false;
}
}
function syncAuthLanguage() {
const dict = getUiDictionary(currentLanguage);
const usernameFields = document.getElementById('registerFields');
const authTitle = document.getElementById('authTitle');
const authButton = document.getElementById('authButton');
const authToggle = document.querySelector('.auth-toggle span');
if (usernameFields) usernameFields.style.display = isRegistering ? 'block' : 'none';
if (authTitle) authTitle.textContent = isRegistering ? translateRawText('Register', dict) : translateRawText('Login', dict);
if (authButton) authButton.textContent = isRegistering ? translateRawText('Create Account', dict) : translateRawText('Login', dict);
if (authToggle) {
const lead = isRegistering ? translateRawText('Already have an account?', dict) : translateRawText("Don't have an account?", dict);
const cta = isRegistering ? translateRawText('Login', dict) : translateRawText('Register', dict);
authToggle.innerHTML = `${lead} `;
const button = authToggle.querySelector('button');
if (button) button.onclick = toggleAuthMode;
}
}
function parseFlexibleNumber(value) {
if (typeof value === 'number') return Number.isFinite(value) ? value : NaN;
if (value === null || value === undefined) return NaN;
const cleaned = String(value).replace(/[^\d.-]/g, '');
if (!cleaned || cleaned === '-' || cleaned === '.' || cleaned === '-.') return NaN;
const parsed = Number(cleaned);
return Number.isFinite(parsed) ? parsed : NaN;
}
function decodeJwtPayload(token) {
try {
if (!token || typeof token !== 'string') return null;
const parts = token.split('.');
if (parts.length < 2) return null;
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - (base64.length % 4 || 4)) % 4);
const json = atob(padded);
const payload = JSON.parse(json);
return payload && typeof payload === 'object' ? payload : null;
} catch (_) {
return null;
}
}
function resolveCurrentUserId() {
if (currentUser?.id) return String(currentUser.id);
try {
const stored = JSON.parse(localStorage.getItem('user') || '{}');
if (stored?.id) {
currentUser = { ...(currentUser || {}), ...stored, id: stored.id };
return String(stored.id);
}
} catch (_) {
}
const payload = decodeJwtPayload(currentToken || localStorage.getItem('token'));
if (payload?.userId) {
currentUser = { ...(currentUser || {}), id: payload.userId };
return String(payload.userId);
}
return null;
}
function getCurrentCoinBalance() {
const candidates = [
currentUser?.coins,
(() => {
try {
const stored = JSON.parse(localStorage.getItem('user') || '{}');
return stored?.coins;
} catch (_) {
return undefined;
}
})(),
document.getElementById('coinsDisplay')?.textContent || ''
];
for (const value of candidates) {
const parsed = parseFlexibleNumber(value);
if (Number.isFinite(parsed)) return parsed;
}
return NaN;
}
function clampPickerScale(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return 1.45;
return Math.min(2, Math.max(1, numeric));
}
function updatePickerScaleLabel() {
const label = document.getElementById('pickerSizeLabel');
if (label) label.textContent = `${Math.round(pickerScale * 100)}%`;
const slider = document.getElementById('pickerSizeRange');
if (slider) slider.value = String(pickerScale);
}
function applyPickerScale() {
const panel = document.getElementById('pickerPanel');
if (!panel) return;
panel.style.setProperty('--picker-scale', String(pickerScale));
updatePickerScaleLabel();
}
function setPickerScale(value) {
pickerScale = clampPickerScale(value);
localStorage.setItem('pickerScale', String(pickerScale));
applyPickerScale();
}
function adjustPickerSize(delta) {
setPickerScale(pickerScale + Number(delta || 0));
}
async function syncCurrentUserCoinsFromProfile() {
if (!currentToken) return null;
const userId = resolveCurrentUserId();
if (!userId) return null;
try {
const response = await fetch(`${API_BASE}/profile/${userId}`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
if (!response.ok) return null;
const user = await response.json();
if (!user || typeof user !== 'object') return null;
currentUser = { ...currentUser, ...user };
localStorage.setItem('user', JSON.stringify(currentUser));
const coinsDisplay = document.getElementById('coinsDisplay');
if (coinsDisplay) {
coinsDisplay.textContent = `💰 Coins: ${user.coins || 0}`;
}
return parseFlexibleNumber(user.coins);
} catch (_) {
return null;
}
}
function updateAllCoinDisplays(coins) {
if (!Number.isFinite(coins)) return;
const coinsDisplay = document.getElementById('coinsDisplay');
if (coinsDisplay) {
coinsDisplay.textContent = `💰 Coins: ${coins}`;
}
const coinElements = document.querySelectorAll('[data-coins-display]');
coinElements.forEach(el => {
el.textContent = `💰 ${coins}`;
});
}
function syncProfilePanelForCurrentRoom() {
const panel = document.getElementById('profilePanel');
if (!panel) return;
if (!currentChatContext || (currentChatContext.type !== 'group' && currentChatContext.type !== 'community')) return;
const roomName = currentChatContext.name;
const profile = currentChatContext.type === 'group'
? getGroupProfile(roomName)
: getCommunityProfile(roomName);
const profileName = document.getElementById('profileName');
const profileAvatar = document.getElementById('profileAvatar');
if (profileName) profileName.textContent = roomName;
if (!profileAvatar) return;
if (profile?.image) {
profileAvatar.style.backgroundImage = `url(${profile.image})`;
profileAvatar.style.backgroundSize = 'cover';
profileAvatar.style.backgroundPosition = 'center';
profileAvatar.textContent = '';
return;
}
profileAvatar.style.backgroundImage = 'none';
profileAvatar.style.backgroundSize = '';
profileAvatar.style.backgroundPosition = '';
profileAvatar.textContent = profile?.icon || (currentChatContext.type === 'group' ? '👥' : '🌐');
}
function refreshRoomVisuals(roomType, roomName, options = {}) {
const { reopenDetails = false } = options;
const dict = getUiDictionary(currentLanguage);
const groupLabel = translateRawText('Group', dict);
const communityLabel = translateRawText('Community', dict);
if (roomType === 'group') {
if (currentChatContext?.type === 'group' && currentChatContext?.name === roomName) {
const profile = getGroupProfile(roomName);
const chatTitle = document.getElementById('chatTitle');
if (chatTitle) chatTitle.textContent = `${profile?.image ? '🖼️' : (profile.icon || '👥')} ${groupLabel}: ${roomName}`;
}
if (!document.getElementById('groupsModal')?.classList.contains('hidden')) {
openGroupsModal();
}
updateAllRoomIconInstances('group', roomName);
} else if (roomType === 'community') {
if (currentChatContext?.type === 'community' && currentChatContext?.name === roomName) {
const profile = getCommunityProfile(roomName);
const chatTitle = document.getElementById('chatTitle');
if (chatTitle) chatTitle.textContent = `${profile?.image ? '🖼️' : (profile.icon || '🌐')} ${communityLabel}: ${roomName}`;
}
if (!document.getElementById('communitiesModal')?.classList.contains('hidden')) {
openCommunitiesModal();
}
if (reopenDetails && document.getElementById('communityDetailsModal')?.classList.contains('show')) {
openCommunityDetails(roomName);
}
updateAllRoomIconInstances('community', roomName);
}
syncProfilePanelForCurrentRoom();
loadFriendsForDM();
}
function updateAllRoomIconInstances(roomType, roomName) {
const profile = roomType === 'group' ? getGroupProfile(roomName) : getCommunityProfile(roomName);
const iconChar = profile?.image ? '🖼️' : (profile?.icon || (roomType === 'group' ? '👥' : '🌐'));
const allChannelItems = document.querySelectorAll('.channel-item');
allChannelItems.forEach(item => {
const label = item.querySelector('.channel-label');
if (label && label.textContent === roomName) {
const avatar = item.querySelector('.channel-avatar');
if (avatar) {
if (profile?.image) {
avatar.style.backgroundImage = `url(${profile.image})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
avatar.textContent = '';
} else {
avatar.style.backgroundImage = 'none';
avatar.textContent = iconChar;
}
}
}
});
if (roomType === 'group') {
const groupCards = document.querySelectorAll('.group-card');
groupCards.forEach(card => {
const nameEl = card.querySelector('.group-name');
if (nameEl && nameEl.textContent === roomName) {
const avatar = card.querySelector('.group-avatar');
if (avatar) {
if (profile?.image) {
avatar.style.backgroundImage = `url(${profile.image})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
avatar.innerHTML = '';
} else {
avatar.style.backgroundImage = 'none';
avatar.innerHTML = `${iconChar}`;
}
}
}
});
}
if (roomType === 'community') {
const communityCards = document.querySelectorAll('.community-card');
communityCards.forEach(card => {
const nameEl = card.querySelector('.group-name');
if (nameEl && nameEl.textContent === roomName) {
const avatar = card.querySelector('.group-avatar');
if (avatar) {
if (profile?.image) {
avatar.style.backgroundImage = `url(${profile.image})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
avatar.innerHTML = '';
} else {
avatar.style.backgroundImage = 'none';
avatar.innerHTML = `${iconChar}`;
}
}
}
});
}
}
function normalizePinnedChats(rawValue) {
const source = Array.isArray(rawValue) ? rawValue : [];
const normalized = [];
source.forEach((entry) => {
if (!entry) return;
if (typeof entry === 'string') {
normalized.push(entry);
return;
}
if (typeof entry === 'object') {
if (entry.type && (entry.id || entry.name)) {
normalized.push(`${entry.type}:${entry.id || entry.name}`);
return;
}
if (entry.id) {
normalized.push(`dm:${entry.id}`);
}
}
});
return Array.from(new Set(normalized));
}
function closeAllMessageActionRows() {
document.querySelectorAll('.message.message-actions-open').forEach((messageEl) => {
messageEl.classList.remove('message-actions-open');
});
}
function ensureMessageActionInteractionBinding() {
if (messageActionInteractionBound) return;
messageActionInteractionBound = true;
document.addEventListener('click', (event) => {
if (!event.target.closest('.message')) {
closeAllMessageActionRows();
}
});
}
function getVisibleMembers(users) {
const list = Array.isArray(users) ? users : [];
const withoutBot = list.filter((user) => String(user?.userId || user?.id || '') !== FIRETECH_BOT_USER.userId);
return [FIRETECH_BOT_USER, ...withoutBot];
}
function applyLanguage(code, silent = false) {
currentLanguage = code;
autoTranslationRequestCount = 0;
localStorage.setItem('appLanguage', code);
const t = I18N[code] || I18N.en;
const lang = LANGUAGES.find(l => l.code === code) || LANGUAGES[0];
document.documentElement.lang = code;
const msgInput = document.getElementById('messageInput');
if (msgInput) msgInput.placeholder = t.typeMsg;
syncAuthLanguage();
const langDisplay = document.getElementById('currentLangDisplay');
if (langDisplay) langDisplay.textContent = `${lang.flag} ${lang.name}`;
localizeStaticUi(code);
if (currentChatContext?.type === 'group' || currentChatContext?.type === 'community') {
refreshRoomVisuals(currentChatContext.type, currentChatContext.name, { reopenDetails: true });
}
if (!document.getElementById('shopModal')?.classList.contains('hidden')) {
loadShopItems();
loadInventory();
}
if (!document.getElementById('friendsModal')?.classList.contains('hidden')) {
loadFriends();
}
if (!document.getElementById('groupsModal')?.classList.contains('hidden')) {
openGroupsModal();
}
if (!document.getElementById('communitiesModal')?.classList.contains('hidden')) {
openCommunitiesModal();
}
if (document.getElementById('communityDetailsModal')?.classList.contains('show') && currentChatContext?.type === 'community') {
openCommunityDetails(currentChatContext.name);
}
if (!document.getElementById('archivedChatsModal')?.classList.contains('hidden')) {
showArchivedChats();
}
applyEnhancedEmoji(document.body);
renderLangGrid();
if (!silent) showToast(`🌐 Language set to ${lang.name}`, 'success');
}
function renderLangGrid() {
const grid = document.getElementById('langGrid');
if (!grid) return;
grid.innerHTML = LANGUAGES.map(l => `
`).join('');
}
function applyEnhancedEmoji(root = document.body) {
if (!root || !window.twemoji || typeof window.twemoji.parse !== 'function') return;
window.twemoji.parse(root, {
folder: 'svg',
ext: '.svg',
className: 'twemoji'
});
}
let localizationObserver = null;
let localizationRefreshTimer = null;
function ensureLocalizationObserver() {
if (localizationObserver) return;
localizationObserver = new MutationObserver(() => {
if (isApplyingLocalization) return;
if (localizationRefreshTimer) clearTimeout(localizationRefreshTimer);
localizationRefreshTimer = setTimeout(() => {
if (isApplyingLocalization) return;
localizeStaticUi(currentLanguage);
applyEnhancedEmoji(document.body);
}, 280);
});
localizationObserver.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['title', 'placeholder']
});
}
let isRegistering = false;
let currentChatFriendId = null;
let currentChatFriendName = null;
let currentChatContext = { type: 'none', id: null, name: null };
let pendingMediaDraft = null;
let pendingMediaDrafts = [];
let activeCallState = null;
let peerConnection = null;
let localCallStream = null;
let remoteCallStream = null;
let remoteIceCandidatesQueue = [];
let lastIceRecoveryAttemptAt = 0;
let lastCallWarningAt = 0;
let currentCallPeerId = null;
let currentCallPeerName = null;
let callTimerInterval = null;
let callStartedAt = null;
let incomingCallAudioContext = null;
let incomingCallInterval = null;
let remoteCallPlaybackRetryBound = false;
let remoteCallPlaybackStarted = false;
let isStartingCallSession = false;
let pendingSignalOffer = null;
let makingOffer = false;
let rtcRuntimeConfig = null;
let rtcConfigLoaded = false;
let activeIceServers = [];
let callIceFailureCount = 0;
let forceRelayOnly = false;
let pendingIceRecoveryTimer = null;
let pendingRelayFallbackTimer = null;
let pendingRemoteIceCandidates = [];
let callEventLog = [];
let currentCallProvider = 'webrtc';
let currentJitsiApi = null;
let currentCallRoomName = null;
let hasUserInteractedWithPage = false;
let activeRoomSubscription = null;
let roomMessages = JSON.parse(localStorage.getItem('roomMessages') || '{}');
let pinnedChats = [];
let joinedCommunities = [];
let joinedGroups = [];
let groupMemberRoles = JSON.parse(localStorage.getItem('groupMemberRoles') || '{}');
let cachedShopItemsById = {};
let equippedShopItems = JSON.parse(localStorage.getItem('equippedShopItems') || '{}');
let baseThemePrimaryColor = localStorage.getItem('baseThemePrimaryColor') || '';
let baseThemePrimaryHover = localStorage.getItem('baseThemePrimaryHover') || '';
let communityProfiles = JSON.parse(localStorage.getItem('communityProfiles') || '{}');
let groupProfiles = JSON.parse(localStorage.getItem('groupProfiles') || '{}');
let selectedOrbEffect = localStorage.getItem('selectedOrbEffect') || 'none';
let isCallCameraOff = false;
let notifList = JSON.parse(localStorage.getItem('notifList') || '[]');
let notifUnreadCount = 0;
let globalOnlineUsers = [];
let currentRoomMembers = [];
let roomRoleCache = JSON.parse(localStorage.getItem('roomRoleCache') || '{}');
let roomProfileCache = JSON.parse(localStorage.getItem('roomProfileCache') || '{}');
const ROOM_CACHE_MAX_PER_ROOM = 40;
const ROOM_CACHE_MEDIA_URL_MAX_CHARS = 1400;
let messageActionInteractionBound = false;
let pickerScale = Number(localStorage.getItem('pickerScale') || '1.45');
let chatSettings = {
disappearTime: 0,
enterToSend: true,
showTypingIndicator: true,
autoplayGifs: true,
compactMode: false,
fontSize: 'medium',
notificationMode: 'all',
notifyOnSent: false,
notifyOnReceived: true,
messageSound: true,
desktopNotifications: false
};
let audioSettings = {
microphoneId: 'default',
speakerId: 'default'
};
let themeSettings = {
mode: 'dark',
customColors: {
primary: '#5865F2',
accent: '#57F287',
background: '#1e1f22',
sidebar: '#2b2d31',
textPrimary: '#ffffff',
textSecondary: '#b5bac1',
iconColor: '#ffffff',
borderColor: '#3f4147'
}
};
const SERVER_URL = window.location.origin;
const API_BASE = SERVER_URL + '/api';
function getDefaultIceServers() {
return [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'stun:stun3.l.google.com:19302' },
{ urls: 'stun:stun4.l.google.com:19302' },
{ urls: 'stun:stun.cloudflare.com:3478' }
];
}
function hasTurnServerConfigured(iceServers = []) {
return iceServers.some((server) => {
const urls = Array.isArray(server?.urls) ? server.urls : [server?.urls];
return urls.some((url) => typeof url === 'string' && /^turns?:/i.test(url));
});
}
async function ensureRtcConfigLoaded() {
if (rtcConfigLoaded) return;
rtcConfigLoaded = true;
try {
const response = await fetch(`${API_BASE}/rtc-config`, { cache: 'no-store' });
if (!response.ok) return;
const data = await response.json();
if (Array.isArray(data?.iceServers) && data.iceServers.length > 0) {
rtcRuntimeConfig = {
iceServers: data.iceServers,
iceTransportPolicy: data.iceTransportPolicy === 'relay' ? 'relay' : 'all'
};
}
} catch (rtcConfigError) {
console.warn('RTC config fetch failed, using defaults:', rtcConfigError);
}
}
function normalizeRoomKeyClient(roomType, roomName) {
return `${String(roomType || '').trim().toLowerCase()}:${String(roomName || '').trim().toLowerCase()}`;
}
function getRoomCacheKey(roomType, roomName) {
return normalizeRoomKeyClient(roomType, roomName);
}
function getMyRoomRole(roomType, roomName) {
const key = getRoomCacheKey(roomType, roomName);
const roleMap = roomRoleCache[key] || {};
const myId = String(currentUser?.id || '');
const cachedRole = roleMap[myId];
if (cachedRole) return cachedRole;
if (roomType === 'group') {
const localRole = groupMemberRoles?.[roomName]?.[myId];
if (localRole) return localRole;
if (joinedGroups.includes(roomName)) return 'Owner';
}
return 'Member';
}
function canManageRoomAppearance(roomType, roomName) {
const role = getMyRoomRole(roomType, roomName);
if (roomType === 'group') {
const localRole = groupMemberRoles?.[roomName]?.[String(currentUser?.id || '')] || 'Member';
if (localRole === 'Owner' || localRole === 'Admin') {
return true;
}
}
if (roomType === 'community') {
const profile = communityProfiles[roomName];
const localRole = Array.isArray(profile?.members)
? (profile.members.find((m) => String(m.id) === String(currentUser?.id))?.role || 'member')
: 'member';
if (localRole === 'leader' || localRole === 'vice_leader') {
return true;
}
}
return role === 'Owner' || role === 'Admin';
}
function cacheRoomProfile(roomType, roomName, profilePatch = {}) {
const key = getRoomCacheKey(roomType, roomName);
const prev = roomProfileCache[key] || {};
roomProfileCache[key] = {
...prev,
...profilePatch,
roomType,
roomName
};
localStorage.setItem('roomProfileCache', JSON.stringify(roomProfileCache));
}
function emitRoomProfileUpdate(roomType, roomName, profilePatch) {
if (!socket || !socket.connected) {
showToast('Not connected to sync room update', 'warning');
return;
}
socket.emit('update room profile', {
roomType,
roomName,
profilePatch
});
}
['pointerdown', 'touchstart', 'keydown'].forEach((eventName) => {
window.addEventListener(eventName, () => {
hasUserInteractedWithPage = true;
}, { passive: true });
});
console.log(`🌐 Connected to: ${SERVER_URL}`);
async function syncBuildVersionBadge() {
try {
const badge = document.getElementById('buildVersionBadge');
if (!badge) return;
const response = await fetch(`${API_BASE}/version`, { cache: 'no-store' });
if (!response.ok) return;
const data = await response.json();
if (data?.version) {
badge.textContent = `v${data.version}`;
badge.title = `Running backend version: ${data.version}`;
}
} catch (error) {
}
}
function startCallTimer() {
stopCallTimer();
callStartedAt = Date.now();
const timerLabel = document.getElementById('callTimerLabel');
if (!timerLabel) return;
callTimerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - callStartedAt) / 1000);
const hrs = String(Math.floor(elapsed / 3600)).padStart(2, '0');
const mins = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0');
const secs = String(elapsed % 60).padStart(2, '0');
timerLabel.textContent = `${hrs}:${mins}:${secs}`;
}, 1000);
}
function stopCallTimer() {
if (callTimerInterval) {
clearInterval(callTimerInterval);
callTimerInterval = null;
}
callStartedAt = null;
}
function startIncomingCallAlert() {
stopIncomingCallAlert();
if (navigator.vibrate && hasUserInteractedWithPage) {
navigator.vibrate([300, 200, 300, 200, 300]);
}
incomingCallInterval = setInterval(() => {
try {
if (!incomingCallAudioContext) {
incomingCallAudioContext = new (window.AudioContext || window.webkitAudioContext)();
}
const osc = incomingCallAudioContext.createOscillator();
const gain = incomingCallAudioContext.createGain();
osc.type = 'sine';
osc.frequency.value = 880;
gain.gain.value = 0.05;
osc.connect(gain);
gain.connect(incomingCallAudioContext.destination);
osc.start();
osc.stop(incomingCallAudioContext.currentTime + 0.15);
} catch (error) {
}
}, 900);
}
function stopIncomingCallAlert() {
if (incomingCallInterval) {
clearInterval(incomingCallInterval);
incomingCallInterval = null;
}
if (navigator.vibrate && hasUserInteractedWithPage) {
try {
navigator.vibrate(0);
} catch (error) {
}
}
}
function formatMessageTimestamp(timestamp) {
const date = timestamp ? new Date(timestamp) : new Date();
if (Number.isNaN(date.getTime())) return 'Unknown time';
return date.toLocaleString([], {
month: 'short',
day: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function ensureRemoteMediaPlayback(videoEl) {
if (!videoEl) return;
const tryPlay = () => {
const playPromise = videoEl.play();
if (!playPromise || typeof playPromise.then !== 'function') return;
playPromise.then(() => {
if (!remoteCallPlaybackStarted) {
remoteCallPlaybackStarted = true;
showToast('📞 Connected! Audio and video are live.', 'success');
}
}).catch((error) => {
if (error?.name === 'AbortError') {
setTimeout(() => {
if (videoEl.srcObject) {
videoEl.play().catch(() => {});
}
}, 120);
return;
}
console.warn('Remote media autoplay blocked, waiting for user gesture:', error);
showToast('Tap once to enable call audio', 'warning');
if (!remoteCallPlaybackRetryBound) {
remoteCallPlaybackRetryBound = true;
const retryPlayback = () => {
videoEl.play().catch((retryErr) => {
console.warn('Retry playback failed:', retryErr);
});
document.removeEventListener('click', retryPlayback);
document.removeEventListener('touchstart', retryPlayback);
document.removeEventListener('pointerdown', retryPlayback);
remoteCallPlaybackRetryBound = false;
};
document.addEventListener('click', retryPlayback, { once: true });
document.addEventListener('touchstart', retryPlayback, { once: true, passive: true });
document.addEventListener('pointerdown', retryPlayback, { once: true });
}
});
};
tryPlay();
setTimeout(tryPlay, 250);
setTimeout(tryPlay, 900);
}
function ensureLocalPreviewPlayback(videoEl) {
if (!videoEl || !videoEl.srcObject) return;
videoEl.muted = true;
videoEl.autoplay = true;
videoEl.playsInline = true;
const tryPlay = () => {
videoEl.play().catch(() => {});
};
tryPlay();
setTimeout(tryPlay, 120);
setTimeout(tryPlay, 500);
}
function appendCallLog(message) {
const entry = `[${new Date().toLocaleTimeString()}] ${message}`;
callEventLog.push(entry);
if (callEventLog.length > 20) {
callEventLog = callEventLog.slice(-20);
}
refreshCallDebug();
}
function setCallStatus(statusText) {
const statusLabel = document.getElementById('callStatusLabel');
if (statusLabel) {
statusLabel.textContent = statusText || '';
}
}
function ensureJitsiApiLoaded() {
if (typeof window.JitsiMeetExternalAPI === 'function') {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const existing = document.querySelector('script[data-jitsi-external-api="true"]');
if (existing) {
existing.addEventListener('load', () => resolve(), { once: true });
existing.addEventListener('error', () => reject(new Error('Failed to load hosted call API')), { once: true });
return;
}
const script = document.createElement('script');
script.src = 'https://meet.jit.si/external_api.js';
script.async = true;
script.dataset.jitsiExternalApi = 'true';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load hosted call API'));
document.head.appendChild(script);
});
}
function setHostedCallVisible(isVisible) {
const host = document.getElementById('hostedCallContainer');
const remoteVideo = document.getElementById('remoteCallVideo');
const localVideo = document.getElementById('localCallVideo');
if (host) host.style.display = isVisible ? 'block' : 'none';
if (remoteVideo) remoteVideo.style.display = isVisible ? 'none' : 'block';
if (localVideo) localVideo.style.display = isVisible ? 'none' : 'block';
}
async function startHostedCallRoom(peerName, roomName) {
if (!roomName) {
throw new Error('Missing hosted room name');
}
openCallDialog(peerName);
setCallStatus('Joining hosted call...');
appendCallLog(`Joining hosted room ${roomName}`);
await ensureJitsiApiLoaded();
const host = document.getElementById('hostedCallContainer');
if (!host) {
throw new Error('Hosted call container not found');
}
if (currentJitsiApi) {
currentJitsiApi.dispose();
currentJitsiApi = null;
}
host.innerHTML = '';
setHostedCallVisible(true);
currentCallProvider = 'hosted';
currentCallRoomName = roomName;
currentJitsiApi = new window.JitsiMeetExternalAPI('meet.jit.si', {
roomName,
parentNode: host,
width: '100%',
height: '100%',
userInfo: {
displayName: currentUser?.username || 'User'
},
configOverwrite: {
prejoinPageEnabled: false,
startWithAudioMuted: false,
startWithVideoMuted: false,
disableDeepLinking: true
},
interfaceConfigOverwrite: {
MOBILE_APP_PROMO: false,
SHOW_JITSI_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false
}
});
currentJitsiApi.addListener('videoConferenceJoined', () => {
setCallStatus('Connected');
appendCallLog('Hosted room joined');
if (!callTimerInterval) {
startCallTimer();
}
});
currentJitsiApi.addListener('participantJoined', (payload) => {
appendCallLog(`Participant joined: ${payload?.displayName || payload?.id || 'remote user'}`);
});
currentJitsiApi.addListener('participantLeft', (payload) => {
appendCallLog(`Participant left: ${payload?.id || 'remote user'}`);
});
currentJitsiApi.addListener('audioMuteStatusChanged', ({ muted }) => {
const btn = document.getElementById('muteBtn');
if (btn) {
btn.textContent = muted ? '🔇' : '🎤';
btn.classList.toggle('muted', !!muted);
}
});
currentJitsiApi.addListener('videoMuteStatusChanged', ({ muted }) => {
const btn = document.getElementById('cameraBtn');
if (btn) {
btn.textContent = muted ? '🚫' : '📷';
btn.classList.toggle('cam-off', !!muted);
}
});
currentJitsiApi.addListener('readyToClose', () => {
appendCallLog('Hosted room requested close');
cleanupCallSession(false);
});
currentJitsiApi.addListener('videoConferenceLeft', () => {
appendCallLog('Hosted room left');
cleanupCallSession(false);
});
}
let callControlBindingsDone = false;
function bindCallControlButtons() {
if (callControlBindingsDone) return;
const bindings = [
['muteBtn', toggleLocalMute],
['cameraBtn', toggleCallCamera],
['endCallBtn', endVideoCall],
['callDebugBtn', toggleCallDebug]
];
bindings.forEach(([id, action]) => {
const button = document.getElementById(id);
if (!button) return;
const handlePress = (event) => {
event.preventDefault();
action();
};
button.addEventListener('click', handlePress);
button.addEventListener('touchend', handlePress, { passive: false });
button.style.pointerEvents = 'auto';
});
callControlBindingsDone = true;
}
function toggleAuthMode() {
isRegistering = !isRegistering;
syncAuthLanguage();
const forgotWrap = document.getElementById('forgotPasswordWrap');
const resetPanel = document.getElementById('resetPasswordPanel');
if (forgotWrap) forgotWrap.style.display = isRegistering ? 'none' : 'block';
if (resetPanel) resetPanel.classList.remove('show');
setAuthFeedback('');
}
function toggleForgotPasswordPanel() {
const panel = document.getElementById('resetPasswordPanel');
if (!panel) return;
panel.classList.toggle('show');
}
function setAuthFeedback(message, type = 'info') {
const statusEl = document.getElementById('authStatus');
if (!statusEl) return;
const colors = {
info: 'var(--text-secondary)',
success: 'var(--success)',
warning: '#f6c945',
error: 'var(--danger)'
};
statusEl.textContent = message || '';
statusEl.style.color = colors[type] || colors.info;
}
async function resetForgottenPassword() {
const email = document.getElementById('resetEmail')?.value.trim();
const newPassword = document.getElementById('resetNewPassword')?.value.trim();
if (!email || !newPassword) {
showToast('Enter your email and new password', 'warning');
setAuthFeedback('Enter your email and new password', 'warning');
return;
}
if (newPassword.length < 6) {
showToast('New password must be at least 6 characters', 'warning');
setAuthFeedback('New password must be at least 6 characters', 'warning');
return;
}
try {
setAuthFeedback('Resetting password...', 'info');
const response = await fetch(`${API_BASE}/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, newPassword })
});
const data = await response.json();
if (!response.ok) {
showToast(data.error || 'Failed to reset password', 'error');
setAuthFeedback(data.error || 'Failed to reset password', 'error');
return;
}
showToast('Password reset successful. You can now login.', 'success');
document.getElementById('resetPasswordPanel')?.classList.remove('show');
document.getElementById('resetEmail').value = email;
document.getElementById('resetNewPassword').value = '';
if (isRegistering) {
toggleAuthMode();
}
if (document.getElementById('email')) {
document.getElementById('email').value = email;
}
if (document.getElementById('password')) {
document.getElementById('password').value = newPassword;
document.getElementById('password').focus();
}
setAuthFeedback('Password reset successful. Use the pre-filled email/password and tap Login.', 'success');
} catch (error) {
console.error(error);
showToast('Error resetting password', 'error');
setAuthFeedback('Error resetting password. Please try again.', 'error');
}
}
async function handleAuth(event) {
event.preventDefault();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value.trim();
const username = document.getElementById('username').value.trim();
const authButton = document.getElementById('authButton');
if (!email || !password) {
showToast('Please fill in all fields', 'warning');
setAuthFeedback('Please fill in all fields', 'warning');
return;
}
try {
if (authButton) {
authButton.disabled = true;
authButton.style.opacity = '0.7';
}
setAuthFeedback(isRegistering ? 'Creating account...' : 'Logging in...', 'info');
let response;
if (isRegistering) {
if (!username) {
showToast('Username is required', 'warning');
setAuthFeedback('Username is required', 'warning');
if (authButton) {
authButton.disabled = false;
authButton.style.opacity = '1';
}
return;
}
response = await fetch(`${API_BASE}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
});
} else {
response = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
}
let data = {};
try {
data = await response.json();
} catch (parseError) {
data = { error: 'Server returned invalid response' };
}
if (!response.ok) {
showToast(data.error || 'Authentication failed', 'error');
setAuthFeedback(data.error || 'Authentication failed', 'error');
return;
}
currentToken = data.token;
currentUser = data.user;
localStorage.setItem('token', currentToken);
localStorage.setItem('user', JSON.stringify(currentUser));
saveAccountSession();
const authSuccessMsg = isRegistering ? 'Registration successful. You are now logged in.' : 'Login successful. Welcome back.';
setAuthFeedback(authSuccessMsg, 'success');
showToast(authSuccessMsg, 'success');
showMainApp();
connectSocket();
} catch (error) {
console.error(error);
showToast('Error: ' + error.message, 'error');
setAuthFeedback(`Error: ${error.message}`, 'error');
} finally {
if (authButton) {
authButton.disabled = false;
authButton.style.opacity = '1';
}
}
}
function logout(preserveAccounts = true) {
if (!preserveAccounts) {
localStorage.removeItem('rememberedAccounts');
}
localStorage.removeItem('token');
localStorage.removeItem('user');
currentUser = null;
currentToken = null;
if (socket) socket.disconnect();
location.reload();
}
function saveAccountSession() {
if (!currentUser || !currentToken) return;
const accounts = JSON.parse(localStorage.getItem('rememberedAccounts') || '[]');
const filtered = accounts.filter(account => account.id !== currentUser.id);
filtered.unshift({
id: currentUser.id,
username: currentUser.username,
email: currentUser.email,
token: currentToken,
user: currentUser
});
localStorage.setItem('rememberedAccounts', JSON.stringify(filtered.slice(0, 5)));
}
function populateAccountSwitcher() {
const switcher = document.getElementById('accountSwitcher');
if (!switcher) return;
const accounts = JSON.parse(localStorage.getItem('rememberedAccounts') || '[]');
switcher.innerHTML = '';
if (accounts.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = 'No saved accounts';
switcher.appendChild(option);
return;
}
accounts.forEach(account => {
const option = document.createElement('option');
option.value = account.id;
option.textContent = `${account.username} (${account.email})`;
if (currentUser && account.id === currentUser.id) {
option.textContent += ' - Current';
}
switcher.appendChild(option);
});
}
function switchAccount() {
const switcher = document.getElementById('accountSwitcher');
if (!switcher) return;
const selectedId = switcher.value;
const accounts = JSON.parse(localStorage.getItem('rememberedAccounts') || '[]');
const selected = accounts.find(account => account.id === selectedId);
if (!selected) {
showToast('Please select an account', 'warning');
return;
}
localStorage.setItem('token', selected.token);
localStorage.setItem('user', JSON.stringify(selected.user));
showToast(`Switched to ${selected.username}`);
setTimeout(() => location.reload(), 400);
}
function connectSocket() {
if (socket && (socket.connected || socket.active)) {
return;
}
if (socket) {
try {
socket.removeAllListeners();
socket.disconnect();
} catch (socketCleanupError) {
console.warn('Previous socket cleanup failed:', socketCleanupError);
}
}
socket = io(SERVER_URL, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: 5,
transports: ['websocket', 'polling']
});
socket.on('connect', () => {
console.log('✅ Connected to server:', SERVER_URL);
socket.emit('join', { token: currentToken });
});
socket.on('users update', (users) => {
globalOnlineUsers = getVisibleMembers(users);
if (currentChatContext?.type === 'group' || currentChatContext?.type === 'community') {
const byId = new Map();
(currentRoomMembers || []).forEach((m) => {
const uid = String(m?.id || m?.userId || '');
if (!uid) return;
byId.set(uid, { ...m });
});
(globalOnlineUsers || []).forEach((u) => {
const uid = String(u?.userId || u?.id || u?._id || '');
if (!uid || byId.has(uid)) return;
byId.set(uid, {
id: uid,
username: u?.username || 'User',
avatar: u?.avatar || null,
role: 'Online'
});
});
renderMembersSidebarFromRoom(Array.from(byId.values()));
} else {
updateMembersList(globalOnlineUsers);
}
});
socket.on('room members update', (payload) => {
const roomType = payload?.roomType;
const roomName = payload?.roomName;
const members = Array.isArray(payload?.members) ? payload.members : [];
const roomKey = getRoomCacheKey(roomType, roomName);
const nextRoles = {};
members.forEach((m) => {
const uid = String(m.id || m.userId || '');
if (uid) nextRoles[uid] = m.role || 'Member';
});
roomRoleCache[roomKey] = nextRoles;
localStorage.setItem('roomRoleCache', JSON.stringify(roomRoleCache));
if (roomType === 'group' && roomName) {
groupMemberRoles[roomName] = nextRoles;
localStorage.setItem('groupMemberRoles', JSON.stringify(groupMemberRoles));
}
const expectedKey = normalizeRoomKeyClient(currentChatContext?.type, currentChatContext?.name);
const incomingKey = normalizeRoomKeyClient(roomType, roomName);
if (expectedKey !== incomingKey) return;
currentRoomMembers = members;
renderMembersSidebarFromRoom(currentRoomMembers);
});
socket.on('room profile updated', (payload) => {
const roomType = payload?.roomType;
const roomName = payload?.roomName;
const previousRoomName = payload?.previousRoomName || roomName;
const profile = payload?.profile || {};
if (!roomType || !roomName) return;
cacheRoomProfile(roomType, roomName, profile);
if (roomType === 'group') {
const previousProfile = groupProfiles[previousRoomName] || groupProfiles[roomName] || {};
const merged = {
...previousProfile,
...profile,
name: profile?.name || roomName
};
if (previousRoomName !== roomName) {
delete groupProfiles[previousRoomName];
if (groupMemberRoles[previousRoomName]) {
groupMemberRoles[roomName] = groupMemberRoles[previousRoomName];
delete groupMemberRoles[previousRoomName];
localStorage.setItem('groupMemberRoles', JSON.stringify(groupMemberRoles));
}
}
groupProfiles[roomName] = merged;
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
const index = joinedGroups.findIndex((g) => String(g) === String(previousRoomName));
if (index >= 0) {
joinedGroups[index] = roomName;
localStorage.setItem('joinedGroups', JSON.stringify(joinedGroups));
}
}
if (roomType === 'community') {
const previousProfile = communityProfiles[previousRoomName] || communityProfiles[roomName] || {};
const merged = {
...previousProfile,
...profile,
name: profile?.name || roomName
};
if (previousRoomName !== roomName) {
delete communityProfiles[previousRoomName];
}
communityProfiles[roomName] = merged;
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
const index = joinedCommunities.findIndex((g) => String(g) === String(previousRoomName));
if (index >= 0) {
joinedCommunities[index] = roomName;
localStorage.setItem('joinedCommunities', JSON.stringify(joinedCommunities));
}
}
if (currentChatContext?.type === roomType && String(currentChatContext?.name) === String(previousRoomName)) {
const isRenameEvent = String(previousRoomName) !== String(roomName);
currentChatContext.name = roomName;
currentChatContext.id = roomName;
if (isRenameEvent) {
const oldKey = normalizeRoomKeyClient(roomType, previousRoomName);
const nextKey = normalizeRoomKeyClient(roomType, roomName);
if (roomMessages[oldKey] && !roomMessages[nextKey]) {
roomMessages[nextKey] = roomMessages[oldKey];
delete roomMessages[oldKey];
saveRoomMessages();
}
if (roomType === 'group') openGroupChat(roomName);
if (roomType === 'community') openCommunityChat(roomName);
} else {
const titleEl = document.getElementById('chatTitle');
if (titleEl) {
const profile = roomType === 'group' ? getGroupProfile(roomName) : getCommunityProfile(roomName);
titleEl.textContent = roomType === 'group'
? `${profile?.image ? '🖼️' : (profile.icon || '👥')} Group: ${roomName}`
: `${profile?.image ? '🖼️' : (profile.icon || '🌐')} Community: ${roomName}`;
}
}
}
if (roomType === 'group' || roomType === 'community') {
const expectedKey = normalizeRoomKeyClient(currentChatContext?.type, currentChatContext?.name);
const incomingKey = normalizeRoomKeyClient(roomType, roomName);
if (expectedKey === incomingKey) {
renderMembersSidebarFromRoom(currentRoomMembers || []);
}
}
loadFriendsForDM();
});
socket.on('room profile updated broadcast', (payload) => {
const roomType = payload?.roomType;
const roomName = payload?.roomName;
const previousRoomName = payload?.previousRoomName || roomName;
const profile = payload?.profile || {};
if (!roomType || !roomName) return;
cacheRoomProfile(roomType, roomName, profile);
if (roomType === 'group') {
const previousProfile = groupProfiles[previousRoomName] || groupProfiles[roomName] || {};
groupProfiles[roomName] = {
...previousProfile,
...profile,
name: profile?.name || roomName
};
if (previousRoomName !== roomName) {
delete groupProfiles[previousRoomName];
}
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
const index = joinedGroups.findIndex((g) => String(g) === String(previousRoomName));
if (index >= 0) {
joinedGroups[index] = roomName;
localStorage.setItem('joinedGroups', JSON.stringify(joinedGroups));
}
}
if (roomType === 'community') {
const previousProfile = communityProfiles[previousRoomName] || communityProfiles[roomName] || {};
communityProfiles[roomName] = {
...previousProfile,
...profile,
name: profile?.name || roomName
};
if (previousRoomName !== roomName) {
delete communityProfiles[previousRoomName];
}
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
const index = joinedCommunities.findIndex((g) => String(g) === String(previousRoomName));
if (index >= 0) {
joinedCommunities[index] = roomName;
localStorage.setItem('joinedCommunities', JSON.stringify(joinedCommunities));
}
}
refreshRoomVisuals(roomType, roomName);
loadFriendsForDM();
});
socket.on('room chat history', (payload) => {
const roomType = payload?.roomType;
const roomName = payload?.roomName;
const roomKey = normalizeRoomKeyClient(roomType, roomName);
roomMessages[roomKey] = Array.isArray(payload?.messages) ? payload.messages : [];
saveRoomMessages();
const isCurrentRoom = normalizeRoomKeyClient(currentChatContext?.type, currentChatContext?.name) === normalizeRoomKeyClient(roomType, roomName);
if (isCurrentRoom) {
renderRoomMessages(roomKey, roomName, roomType);
}
});
socket.on('room message', (payload) => {
const roomType = payload?.roomType;
const roomName = payload?.roomName;
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKeyClient(roomType, roomName);
if (!roomMessages[roomKey]) {
roomMessages[roomKey] = [];
}
const msgId = payload?.messageId || payload?.id;
const exists = msgId ? roomMessages[roomKey].some((m) => String(m?.messageId || m?.id) === String(msgId)) : false;
if (!exists) {
roomMessages[roomKey].push(payload);
}
saveRoomMessages();
const isCurrentRoom = normalizeRoomKeyClient(currentChatContext?.type, currentChatContext?.name) === normalizeRoomKeyClient(roomType, roomName);
if (isCurrentRoom) {
const alreadyRendered = payload?.messageId
? !!document.querySelector(`.message[data-message-id="${String(payload.messageId)}"]`)
: false;
if (!alreadyRendered) {
addMessageToChat(payload);
}
} else if (payload.from !== currentUser.id && chatSettings.notifyOnReceived) {
const messagePreview = payload.mediaType === 'text' ? payload.content : `${payload.mediaType} message`;
const roomLabel = roomType === 'community' ? 'Community' : 'Group';
showToastClickable(
`New message in ${roomLabel} "${roomName}" from ${payload.fromUsername}: ${messagePreview}`,
'info',
{ type: roomType, name: roomName }
);
playNotificationSound();
pushNotification(`${roomLabel === 'Community' ? '🌐' : '👥'} ${roomName}`, `${payload.fromUsername}: ${messagePreview}`, { type: roomType, name: roomName });
}
});
socket.on('room chat cleared', (payload) => {
const roomType = payload?.roomType;
const roomName = payload?.roomName;
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKeyClient(roomType, roomName);
roomMessages[roomKey] = [];
saveRoomMessages();
const isCurrentRoom = normalizeRoomKeyClient(currentChatContext?.type, currentChatContext?.name) === normalizeRoomKeyClient(roomType, roomName);
if (isCurrentRoom) {
document.getElementById('messages-container').innerHTML = '';
refreshChatScrollbar();
}
});
socket.on('dm history cleared', (payload) => {
const fromUserId = payload?.fromUserId;
if (!fromUserId) return;
if (currentChatContext?.type === 'dm' && String(currentChatContext?.id) === String(fromUserId)) {
document.getElementById('messages-container').innerHTML = '';
refreshChatScrollbar();
showToast('Chat history was cleared', 'info');
}
});
socket.on('dm message', (data) => {
const fromId = String(data.from);
const toId = String(data.to);
const myId = String(currentUser.id);
const isSender = fromId === myId;
const otherUserId = isSender ? toId : fromId;
const isCurrentChat = (
currentChatContext.type === 'dm' &&
currentChatContext.id &&
String(currentChatContext.id) === String(otherUserId)
);
if (isCurrentChat) {
if (!isSender) {
addMessageToChat(data);
}
}
if (isSender && chatSettings.notifyOnSent) {
}
if (!isSender && chatSettings.notifyOnReceived) {
const messagePreview = data.mediaType === 'text' ? data.content : `${data.mediaType} message`;
showToastClickable(
`New message from ${data.fromUsername}: ${messagePreview}`,
'info',
{ type: 'dm', friendId: otherUserId, friendName: data.fromUsername }
);
showDesktopNotification(`New message from ${data.fromUsername}`, messagePreview, false);
playNotificationSound();
pushNotification(`💬 ${data.fromUsername}`, messagePreview, { type: 'dm', friendId: otherUserId, friendName: data.fromUsername });
}
});
socket.on('dm delivery status', (data) => {
if (data && data.delivered === false) {
showToast('Message sent, but recipient is currently offline', 'warning');
}
});
socket.on('dm user typing', (data) => {
showTypingIndicator(data.username);
});
socket.on('dm user stop typing', (data) => {
hideTypingIndicator(data.username);
});
socket.on('community user typing', (data) => {
if (currentChatContext.type === 'community' && currentChatContext.name === data.community) {
showTypingIndicator(data.username);
}
});
socket.on('community user stop typing', (data) => {
if (currentChatContext.type === 'community' && currentChatContext.name === data.community) {
hideTypingIndicator();
}
});
socket.on('room user typing', (data) => {
if (currentChatContext.type === 'group' && String(currentChatContext.id) === String(data.roomId)) {
showTypingIndicator(data.username);
}
});
socket.on('room user stop typing', (data) => {
if (currentChatContext.type === 'group' && String(currentChatContext.id) === String(data.roomId)) {
hideTypingIndicator();
}
});
socket.on('error', (msg) => {
console.error('Socket error:', msg);
showToast(msg || 'Realtime error', 'error');
});
socket.on('friend request received', (payload) => {
const senderName = payload?.fromUsername || 'Someone';
showToast(`New friend request from ${senderName}`, 'warning');
if (document.getElementById('friendsModal') && !document.getElementById('friendsModal').classList.contains('hidden')) {
loadFriends();
}
});
socket.on('friend request accepted', (payload) => {
if (!currentUser) return;
if (String(payload?.fromId) === String(currentUser.id) || String(payload?.toId) === String(currentUser.id)) {
loadFriendsForDM();
if (document.getElementById('friendsModal') && !document.getElementById('friendsModal').classList.contains('hidden')) {
loadFriends();
}
}
});
socket.on('friend request rejected', (payload) => {
if (!currentUser) return;
if (String(payload?.fromId) === String(currentUser.id) || String(payload?.toId) === String(currentUser.id)) {
if (document.getElementById('friendsModal') && !document.getElementById('friendsModal').classList.contains('hidden')) {
loadFriends();
}
}
});
socket.on('incoming video call', (payload) => {
handleIncomingVideoCall(payload);
});
socket.on('video call ringing', (payload) => {
const targetName = currentChatContext?.name || 'user';
activeCallState = { callId: payload.callId, peerId: payload.toId, direction: 'outgoing' };
showToast(`Calling ${targetName}...`, 'success');
});
socket.on('video call accepted', (payload) => {
const byName = payload?.byUsername || 'User';
stopIncomingCallAlert();
showToast(`${byName} accepted the call`, 'success');
const roomName = activeCallState?.callId || payload?.callId;
if (roomName) {
closeCustomDialog();
startHostedCallRoom(byName, `firetech-${roomName}`).catch((error) => {
console.error('Failed to start hosted call session:', error);
showToast('Unable to join hosted call', 'error');
cleanupCallSession(false);
});
}
});
socket.on('video call rejected', (payload) => {
const byName = payload?.byUsername || 'User';
stopIncomingCallAlert();
showToast(`${byName} declined the call`, 'warning');
activeCallState = null;
closeCustomDialog();
document.getElementById('callOverlay')?.classList.remove('active');
});
socket.on('video call unavailable', () => {
stopIncomingCallAlert();
showToast('User is currently unavailable for calls', 'warning');
cleanupCallSession(false);
});
socket.on('video call ended', (payload) => {
const byName = payload?.byUsername || 'User';
stopIncomingCallAlert();
showToast(`Call ended by ${byName}`, 'warning');
cleanupCallSession(false);
});
socket.on('video signal', async (payload) => {
const signal = payload?.signal;
const fromId = payload?.fromId;
const fromName = payload?.fromUsername || 'User';
if (!signal || !fromId) return;
try {
if (signal.type === 'offer') {
const isPolitePeer = String(currentUser?.id || '') > String(fromId || '');
const hasPeer = !!peerConnection;
const offerCollision = hasPeer && (makingOffer || peerConnection.signalingState !== 'stable');
if (offerCollision) {
if (!isPolitePeer) {
console.warn('Ignoring offer collision (impolite peer)');
return;
}
if (peerConnection.signalingState === 'have-local-offer') {
await peerConnection.setLocalDescription({ type: 'rollback' });
}
}
if (!activeCallState) {
activeCallState = { callId: payload?.callId || null, peerId: fromId, direction: 'incoming' };
}
if (isStartingCallSession) {
pendingSignalOffer = { fromId, offer: signal.offer };
return;
}
if (!peerConnection) {
await startCallSession(fromName, fromId, false);
}
if (!peerConnection) return;
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.offer));
await flushQueuedIceCandidates();
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit('video signal', {
toId: fromId,
callId: activeCallState?.callId,
signal: { type: 'answer', answer }
});
}
if (signal.type === 'answer' && peerConnection) {
if (peerConnection.signalingState !== 'have-local-offer') {
console.warn('Ignoring stale answer, signaling state:', peerConnection.signalingState);
} else {
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.answer));
await flushQueuedIceCandidates();
}
}
if (signal.type === 'ice-candidate' && signal.candidate) {
await addOrQueueIceCandidate(signal.candidate);
}
} catch (error) {
console.error('Video signaling error:', error);
showToast('Video call signaling failed', 'error');
}
});
}
function showTypingIndicator(username) {
if (!chatSettings.showTypingIndicator) return;
const typingDiv = document.getElementById('typingIndicator');
if (typingDiv) {
typingDiv.textContent = `${username} is typing...`;
typingDiv.style.display = 'block';
}
}
function hideTypingIndicator() {
const typingDiv = document.getElementById('typingIndicator');
if (typingDiv) {
typingDiv.style.display = 'none';
}
}
function showMainApp() {
document.getElementById('authContainer').style.display = 'none';
document.getElementById('mainApp').classList.remove('hidden');
try {
const profilePanel = document.getElementById('profilePanel');
if (profilePanel) {
profilePanel.classList.remove('show');
profilePanel.removeAttribute('style');
}
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel) {
settingsPanel.classList.remove('show');
settingsPanel.removeAttribute('style');
}
const mobileMembersPanel = document.getElementById('mobileMembersPanel');
if (mobileMembersPanel) {
mobileMembersPanel.classList.remove('show');
}
const sidebarOverlay = document.getElementById('sidebarOverlay');
if (sidebarOverlay) {
sidebarOverlay.classList.remove('show');
}
const channelSidebar = document.getElementById('channelSidebar');
if (channelSidebar) {
channelSidebar.classList.remove('show');
}
} catch (e) {
console.log('Error closing panels:', e);
}
loadUserProfile().finally(() => {
loadShopItems();
});
const urlParams = new URLSearchParams(window.location.search);
const addFriendId = urlParams.get('addFriend');
if (addFriendId && addFriendId !== currentUser.id) {
sendFriendRequestFromLink(addFriendId);
window.history.replaceState({}, document.title, window.location.pathname);
}
}
function showSection(section) {
if (section === 'chat') {
document.querySelector('.chat-area').style.display = 'flex';
document.getElementById('friendsModal').classList.add('hidden');
document.getElementById('shopModal').classList.add('hidden');
if (currentChatContext.type === 'community' || currentChatContext.type === 'group') {
subscribeToRoomChat(currentChatContext.type, currentChatContext.name);
}
loadFriendsForDM();
} else if (section === 'friends') {
document.querySelector('.chat-area').style.display = 'none';
loadFriends();
document.getElementById('friendsModal').classList.remove('hidden');
} else if (section === 'shop') {
document.querySelector('.chat-area').style.display = 'none';
loadUserProfile().finally(() => {
loadShopItems();
});
document.getElementById('shopModal').classList.remove('hidden');
}
localizeStaticUi(currentLanguage);
}
function openDM(friendId, friendName) {
if (activeRoomSubscription && socket && socket.connected) {
socket.emit('leave room chat', activeRoomSubscription);
}
activeRoomSubscription = null;
currentChatFriendId = friendId;
currentChatFriendName = friendName;
currentChatContext = { type: 'dm', id: friendId, name: friendName };
clearPendingMediaDraft();
updatePinButtonState();
const isSelfChat = String(friendId) === String(currentUser.id);
const isBotChat = String(friendId) === FIRETECH_BOT_USER.userId;
const displayName = isSelfChat ? `📝 ${currentUser.username} (Notes)` : isBotChat ? '⚡ FireTech Bot' : `💬 ${friendName}`;
const chatTitleEl = document.getElementById('chatTitle');
chatTitleEl.textContent = displayName;
chatTitleEl.onclick = null;
document.getElementById('messages-container').innerHTML = '';
refreshChatScrollbar();
document.getElementById('closeChatBtn').style.display = 'block';
loadDMMessages(friendId);
if (socket && socket.connected) {
socket.emit('join dm', friendId);
}
currentRoomMembers = [];
if (globalOnlineUsers.length > 0) {
updateMembersList(globalOnlineUsers);
}
showSection('chat');
if (window.innerWidth <= 480) {
closeMobileSidebar();
}
}
async function loadDMMessages(friendId) {
if (String(friendId) === FIRETECH_BOT_USER.userId) {
document.getElementById('messages-container').innerHTML = '';
addMessageToChat({
from: FIRETECH_BOT_USER.userId,
fromUsername: FIRETECH_BOT_USER.username,
fromAvatar: null,
to: currentUser?.id,
content: 'Type /firetech or @firetech for commands. I provide security tips, encryption info, and network insights.',
mediaType: 'text',
timestamp: new Date().toISOString(),
isBot: true
});
refreshChatScrollbar();
return;
}
try {
const response = await fetch(`${API_BASE}/dms/${friendId}`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
const messages = await response.json();
document.getElementById('messages-container').innerHTML = '';
messages.forEach(msg => {
addMessageToChat({
from: msg.from,
fromUsername: msg.fromUsername,
fromAvatar: msg.fromAvatar || null,
content: msg.content,
mediaType: msg.mediaType,
mediaUrl: msg.mediaUrl,
timestamp: msg.timestamp
});
});
refreshChatScrollbar();
} catch (error) {
console.error('Failed to load DM messages:', error);
}
}
function toggleChannelSidebar() {
const sidebar = document.getElementById('channelSidebar');
const overlay = document.getElementById('sidebarOverlay');
sidebar?.classList.toggle('show');
if (overlay) {
overlay.classList.toggle('show');
}
}
function openProfile() {
const panel = document.getElementById('profilePanel');
if (!panel) return;
panel.style.removeProperty('display');
panel.style.removeProperty('visibility');
panel.style.removeProperty('pointer-events');
panel.style.removeProperty('opacity');
panel.style.removeProperty('transform');
panel.style.overflowY = 'auto';
panel.style.webkitOverflowScrolling = 'touch';
panel.style.height = '100dvh';
panel.style.maxHeight = '100dvh';
panel.style.touchAction = 'pan-y';
panel.classList.add('show');
panel.scrollTop = 0;
const backdrop = document.getElementById('profileBackdrop');
if (backdrop) backdrop.style.display = 'block';
if (currentChatContext?.type === 'group' || currentChatContext?.type === 'community') {
const isGroup = currentChatContext.type === 'group';
const profile = isGroup ? getGroupProfile(currentChatContext.name) : getCommunityProfile(currentChatContext.name);
const profileName = document.getElementById('profileName');
const profileBio = document.getElementById('profileBio');
const profileAvatar = document.getElementById('profileAvatar');
if (profileName) profileName.textContent = currentChatContext.name;
if (profileBio) profileBio.textContent = isGroup ? 'Group profile' : 'Community profile';
if (profileAvatar) {
if (profile?.image) {
profileAvatar.style.backgroundImage = `url(${profile.image})`;
profileAvatar.style.backgroundSize = 'cover';
profileAvatar.style.backgroundPosition = 'center';
profileAvatar.textContent = '';
} else {
profileAvatar.style.backgroundImage = 'none';
profileAvatar.style.backgroundSize = '';
profileAvatar.style.backgroundPosition = '';
profileAvatar.textContent = profile?.icon || (isGroup ? '👥' : '🌐');
}
}
} else {
loadUserProfile();
}
}
function shareProfileLink() {
const profileUrl = `${window.location.origin}?addFriend=${currentUser.id}`;
navigator.clipboard.writeText(profileUrl).then(() => {
showToast('Profile link copied to clipboard! Share with friends to connect.');
}).catch(() => {
const tempInput = document.createElement('input');
tempInput.value = profileUrl;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
showToast('Profile link copied!');
});
}
const BANNER_PRESETS = [
'https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&q=80',
'https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80',
'https://images.unsplash.com/photo-1528459801416-a9e53bbf4e17?w=800&q=80',
'https://images.unsplash.com/photo-1550684376-efcbd6e3f031?w=800&q=80',
'#gradient:135,#667eea,#764ba2',
'#gradient:135,#f093fb,#f5576c',
'#gradient:135,#4facfe,#00f2fe',
'#gradient:135,#43e97b,#38f9d7',
];
function openBannerPicker() {
const presetsHtml = BANNER_PRESETS.map((p, i) => {
const isGradient = p.startsWith('#gradient:');
const style = isGradient
? (() => { const parts = p.replace('#gradient:','').split(','); return `background: linear-gradient(${parts[0]}deg, ${parts[1]}, ${parts[2]})`; })()
: `background-image: url('${p}'); background-size: cover; background-position: center;`;
return `
`;
}).join('');
const html = `
`;
customAlert(html, '🖼 Profile Banner', '🖼');
}
function applyBannerPreset(preset) {
const wrap = document.getElementById('profileBannerWrap');
const img = document.getElementById('profileBannerImg');
if (!wrap) return;
if (preset.startsWith('#gradient:')) {
const parts = preset.replace('#gradient:', '').split(',');
wrap.style.background = `linear-gradient(${parts[0]}deg, ${parts[1]}, ${parts[2]})`;
img.style.display = 'none';
} else {
img.src = preset;
img.style.display = 'block';
wrap.style.background = '';
}
localStorage.setItem('profileBanner', preset);
closeCustomDialog();
}
function handleBannerUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
applyBannerPreset(e.target.result);
};
reader.readAsDataURL(file);
event.target.value = '';
}
function clearBanner() {
const wrap = document.getElementById('profileBannerWrap');
const img = document.getElementById('profileBannerImg');
if (wrap) wrap.style.background = '';
if (img) { img.style.display = 'none'; img.src = ''; }
localStorage.removeItem('profileBanner');
}
function loadSavedBanner() {
const saved = localStorage.getItem('profileBanner');
if (saved) applyBannerPreset(saved);
}
function filterGroupsList(query) {
const items = document.querySelectorAll('#groupsList .group-card');
const q = query.toLowerCase();
items.forEach(card => {
const name = card.querySelector('.group-name')?.textContent?.toLowerCase() || '';
card.style.display = name.includes(q) ? '' : 'none';
});
}
function closeProfile() {
const panel = document.getElementById('profilePanel');
if (!panel) return;
panel.classList.remove('show');
const backdrop = document.getElementById('profileBackdrop');
if (backdrop) backdrop.style.display = 'none';
}
function switchProfileTab(tab, triggerEl) {
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
const tabTrigger = triggerEl || (typeof event !== 'undefined' ? event.target : null);
if (tabTrigger) {
tabTrigger.classList.add('active');
}
document.getElementById(tab + 'Tab').classList.add('active');
if (tab === 'inventory') {
loadInventory();
} else if (tab === 'orbs') {
renderOrbEffects();
} else if (tab === 'saved') {
renderSavedItemsSummary();
}
}
function applyOrbEffect(effect = 'none', options = {}) {
const { silent = false } = options;
const root = document.documentElement;
root.classList.remove('orb-none', 'orb-aurora', 'orb-neon-pulse', 'orb-ocean-mist', 'orb-ember');
const classMap = {
none: 'orb-none',
aurora: 'orb-aurora',
neon_pulse: 'orb-neon-pulse',
ocean_mist: 'orb-ocean-mist',
ember: 'orb-ember'
};
const normalized = classMap[effect] ? effect : 'none';
root.classList.add(classMap[normalized]);
selectedOrbEffect = normalized;
localStorage.setItem('selectedOrbEffect', normalized);
const orbSelect = document.getElementById('orbEffectSelect');
if (orbSelect) {
orbSelect.value = normalized;
}
if (!silent) {
showToast('Orb effect updated', 'success');
}
}
function renderOrbEffects() {
const orbSelect = document.getElementById('orbEffectSelect');
if (orbSelect) {
orbSelect.value = selectedOrbEffect;
}
applyOrbEffect(selectedOrbEffect, { silent: true });
}
function renderSavedItemsSummary() {
const savedMediaCountEl = document.getElementById('savedMediaCount');
const archivedMessageCountEl = document.getElementById('savedArchivedCount');
if (savedMediaCountEl) {
savedMediaCountEl.textContent = String(copiedMediaItems.length);
}
if (archivedMessageCountEl) {
archivedMessageCountEl.textContent = String(archivedMessages.length);
}
}
function closeFriendsModal() {
document.getElementById('friendsModal').classList.add('hidden');
showSection('chat');
}
function closeShopModal() {
document.getElementById('shopModal').classList.add('hidden');
showSection('chat');
}
async function loadUserProfile() {
try {
const response = await fetch(`${API_BASE}/profile/${currentUser.id}`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
const user = await response.json();
const profileAvatar = document.getElementById('profileAvatar');
if (user.avatar) {
profileAvatar.style.backgroundImage = `url(${user.avatar})`;
profileAvatar.style.backgroundSize = 'cover';
profileAvatar.style.backgroundPosition = 'center';
profileAvatar.textContent = '';
} else {
profileAvatar.style.backgroundImage = 'none';
profileAvatar.textContent = user.username.charAt(0).toUpperCase();
}
document.getElementById('profileName').textContent = user.username;
document.getElementById('profileBio').textContent = user.bio || 'No bio yet';
document.getElementById('editUsername').value = user.username;
document.getElementById('editBio').value = user.bio;
const phoneInput = document.getElementById('editPhoneNumber');
if (phoneInput) {
phoneInput.value = user.phoneNumber || '';
}
document.getElementById('coinsDisplay').textContent = `💰 Coins: ${user.coins}`;
currentUser = user;
localStorage.setItem('user', JSON.stringify(user));
applyPersistedShopEffects();
} catch (error) {
console.error('Failed to load profile:', error);
}
}
async function saveProfile() {
try {
const username = document.getElementById('editUsername').value;
const bio = document.getElementById('editBio').value;
const phoneNumber = document.getElementById('editPhoneNumber')?.value || '';
const response = await fetch(`${API_BASE}/profile`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ username, bio, phoneNumber, avatar: currentUser.avatar || '' })
});
const data = await response.json();
if (response.ok) {
showToast('Profile updated successfully!');
loadUserProfile();
} else {
showToast(data.error, 'error');
}
} catch (error) {
console.error(error);
showToast('Error saving profile', 'error');
}
}
function handleMessageKeypress(event) {
if (event.key === 'Enter' && chatSettings.enterToSend) {
sendMessage();
}
}
function renderPendingMediaDraft() {
const panel = document.getElementById('mediaDraftPreview');
if (!panel) return;
if (pendingMediaDrafts.length === 0) {
panel.style.display = 'none';
return;
}
panel.style.display = 'flex';
const leftDiv = panel.querySelector('.draft-preview-left') || document.createElement('div');
leftDiv.className = 'draft-preview-left';
leftDiv.style.display = 'flex';
leftDiv.style.gap = '10px';
leftDiv.style.flexWrap = 'wrap';
leftDiv.style.flex = '1';
leftDiv.innerHTML = '';
pendingMediaDrafts.forEach((draft, idx) => {
const itemWrapper = document.createElement('div');
itemWrapper.style.position = 'relative';
itemWrapper.style.display = 'inline-block';
if (draft.mediaType === 'image' || draft.mediaType === 'gif') {
const img = document.createElement('img');
img.src = draft.mediaUrl;
img.style.maxWidth = '70px';
img.style.maxHeight = '70px';
img.style.borderRadius = '6px';
img.style.border = '2px solid var(--primary-color)';
itemWrapper.appendChild(img);
} else if (draft.mediaType === 'sticker') {
const emoji = document.createElement('span');
emoji.textContent = draft.content || '😀';
emoji.style.fontSize = '48px';
emoji.style.display = 'flex';
emoji.style.alignItems = 'center';
emoji.style.justifyContent = 'center';
emoji.style.width = '70px';
emoji.style.height = '70px';
emoji.style.borderRadius = '6px';
emoji.style.background = 'var(--chat-bg)';
emoji.style.border = '2px solid var(--primary-color)';
itemWrapper.appendChild(emoji);
} else if (draft.mediaType === 'video') {
const vid = document.createElement('div');
vid.textContent = '▶️ Video';
vid.style.width = '70px';
vid.style.height = '70px';
vid.style.borderRadius = '6px';
vid.style.background = 'var(--chat-bg)';
vid.style.border = '2px solid var(--primary-color)';
vid.style.display = 'flex';
vid.style.alignItems = 'center';
vid.style.justifyContent = 'center';
vid.style.fontSize = '24px';
itemWrapper.appendChild(vid);
} else {
const file = document.createElement('div');
file.textContent = '📎 File';
file.style.width = '70px';
file.style.height = '70px';
file.style.borderRadius = '6px';
file.style.background = 'var(--chat-bg)';
file.style.border = '2px solid var(--primary-color)';
file.style.display = 'flex';
file.style.alignItems = 'center';
file.style.justifyContent = 'center';
file.style.fontSize = '20px';
itemWrapper.appendChild(file);
}
const removeBtn = document.createElement('button');
removeBtn.innerHTML = '✕';
removeBtn.style.position = 'absolute';
removeBtn.style.top = '-8px';
removeBtn.style.right = '-8px';
removeBtn.style.width = '28px';
removeBtn.style.height = '28px';
removeBtn.style.borderRadius = '50%';
removeBtn.style.background = 'var(--primary-color)';
removeBtn.style.color = 'white';
removeBtn.style.border = 'none';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontSize = '16px';
removeBtn.style.fontWeight = 'bold';
removeBtn.style.display = 'flex';
removeBtn.style.alignItems = 'center';
removeBtn.style.justifyContent = 'center';
removeBtn.style.transition = 'transform 0.1s';
removeBtn.onclick = () => removeMediaDraft(idx);
removeBtn.onmouseover = () => removeBtn.style.transform = 'scale(1.2)';
removeBtn.onmouseout = () => removeBtn.style.transform = 'scale(1)';
itemWrapper.appendChild(removeBtn);
leftDiv.appendChild(itemWrapper);
});
if (!panel.querySelector('.draft-preview-left')) {
const existingLeft = panel.querySelector('.draft-preview-left');
if (existingLeft) existingLeft.remove();
panel.insertBefore(leftDiv, panel.firstChild);
} else {
panel.querySelector('.draft-preview-left').innerHTML = leftDiv.innerHTML;
}
}
function removeMediaDraft(index) {
pendingMediaDrafts.splice(index, 1);
pendingMediaDraft = pendingMediaDrafts[0] || null;
if (pendingMediaDrafts.length === 0) {
document.getElementById('mediaDraftPreview').style.display = 'none';
}
renderPendingMediaDraft();
showToast('Media item removed', 'info');
}
function setPendingMediaDraft(draft) {
pendingMediaDrafts.push(draft);
pendingMediaDraft = pendingMediaDrafts[0] || null;
renderPendingMediaDraft();
}
function clearPendingMediaDraft() {
pendingMediaDrafts = [];
pendingMediaDraft = null;
renderPendingMediaDraft();
}
function scrollChatUp() {
const container = document.getElementById('messages-container');
if (!container) return;
container.scrollBy({ top: -220, behavior: 'smooth' });
}
function scrollChatDown() {
const container = document.getElementById('messages-container');
if (!container) return;
container.scrollBy({ top: 220, behavior: 'smooth' });
}
let isDraggingChatThumb = false;
let chatThumbDragOffset = 0;
function refreshChatScrollbar() {
const container = document.getElementById('messages-container');
const rail = document.getElementById('chatScrollRail');
const thumb = document.getElementById('chatScrollThumb');
if (!container || !rail || !thumb) return;
const maxScroll = container.scrollHeight - container.clientHeight;
rail.classList.remove('hidden');
const trackHeight = rail.clientHeight;
const ratio = container.scrollHeight > 0 ? (container.clientHeight / container.scrollHeight) : 1;
const thumbHeight = Math.max(56, Math.round(Math.min(1, ratio) * trackHeight));
thumb.style.height = `${Math.min(trackHeight, thumbHeight)}px`;
syncChatScrollbarPosition();
}
function syncChatScrollbarPosition() {
const container = document.getElementById('messages-container');
const rail = document.getElementById('chatScrollRail');
const thumb = document.getElementById('chatScrollThumb');
if (!container || !rail || !thumb || rail.classList.contains('hidden')) return;
const maxScroll = container.scrollHeight - container.clientHeight;
const maxThumbTravel = rail.clientHeight - thumb.offsetHeight;
if (maxScroll <= 0 || maxThumbTravel <= 0) {
thumb.style.transform = 'translateY(0px)';
return;
}
const nextTop = (container.scrollTop / maxScroll) * maxThumbTravel;
thumb.style.transform = `translateY(${nextTop}px)`;
}
function startChatScrollbarDrag(event) {
const thumb = document.getElementById('chatScrollThumb');
if (!thumb) return;
isDraggingChatThumb = true;
chatThumbDragOffset = event.clientY - thumb.getBoundingClientRect().top;
event.preventDefault();
}
function stopChatScrollbarDrag() {
isDraggingChatThumb = false;
}
function handleChatScrollbarDrag(event) {
if (!isDraggingChatThumb) return;
const container = document.getElementById('messages-container');
const rail = document.getElementById('chatScrollRail');
const thumb = document.getElementById('chatScrollThumb');
if (!container || !rail || !thumb) return;
const maxThumbTravel = rail.clientHeight - thumb.offsetHeight;
const maxScroll = container.scrollHeight - container.clientHeight;
if (maxThumbTravel <= 0 || maxScroll <= 0) return;
const railTop = rail.getBoundingClientRect().top;
const rawTop = event.clientY - railTop - chatThumbDragOffset;
const clampedTop = Math.max(0, Math.min(maxThumbTravel, rawTop));
container.scrollTop = (clampedTop / maxThumbTravel) * maxScroll;
syncChatScrollbarPosition();
}
function jumpChatScrollbar(event) {
if (event.target && event.target.id === 'chatScrollThumb') return;
const container = document.getElementById('messages-container');
const rail = document.getElementById('chatScrollRail');
const thumb = document.getElementById('chatScrollThumb');
if (!container || !rail || !thumb) return;
const maxThumbTravel = rail.clientHeight - thumb.offsetHeight;
const maxScroll = container.scrollHeight - container.clientHeight;
if (maxThumbTravel <= 0 || maxScroll <= 0) return;
const clickY = event.clientY - rail.getBoundingClientRect().top;
const desiredTop = Math.max(0, Math.min(maxThumbTravel, clickY - (thumb.offsetHeight / 2)));
container.scrollTop = (desiredTop / maxThumbTravel) * maxScroll;
syncChatScrollbarPosition();
}
async function sendMessage() {
const input = document.getElementById('messageInput');
const content = input.value.trim();
const hasDrafts = pendingMediaDrafts.length > 0;
if (!content && !hasDrafts) {
showToast('Type a message or pick media first', 'warning');
return;
}
try {
if (content) {
updateUserStats('messagesCount', 1);
updateUserStats('totalChars', content.length);
}
if (currentChatContext.type === 'community' || currentChatContext.type === 'group') {
if (hasDrafts) {
pendingMediaDrafts.forEach((draft, index) => {
sendRoomMessage({
content: index === 0 && content ? content : (draft.content || (draft.mediaType === 'image' ? 'Photo' : 'Media')),
mediaType: draft.mediaType,
mediaUrl: draft.mediaUrl || null
});
});
} else {
sendRoomMessage({ content, mediaType: 'text', mediaUrl: null });
}
clearPendingMediaDraft();
input.value = '';
hideTypingIndicator();
return;
}
const targetId = ensureChatTarget();
if (!targetId) {
showToast('Unable to find chat target', 'warning');
return;
}
if (hasDrafts) {
for (let index = 0; index < pendingMediaDrafts.length; index++) {
const draft = pendingMediaDrafts[index];
await sendStagedDm(targetId, {
content: index === 0 && content ? content : (draft.content || (draft.mediaType === 'image' ? 'Photo' : 'Media')),
mediaType: draft.mediaType,
mediaUrl: draft.mediaUrl || null
});
}
clearPendingMediaDraft();
} else {
if (!socket || !socket.connected) {
showToast('Connection lost. Reconnecting...', 'warning');
connectSocket();
return;
}
const optimisticMessage = {
id: Date.now(),
from: currentUser.id,
fromUsername: currentUser.username,
fromAvatar: currentUser.avatar,
to: targetId,
content: content,
mediaType: 'text',
timestamp: new Date().toISOString()
};
addMessageToChat(optimisticMessage);
socket.emit('send dm', {
toUserId: targetId,
content: content,
mediaType: 'text'
});
}
input.value = '';
hideTypingIndicator();
} catch (error) {
console.error(error);
}
}
function addMessageToChat(data) {
ensureMessageActionInteractionBinding();
const container = document.getElementById('messages-container');
const messageDiv = document.createElement('div');
messageDiv.className = 'message';messageDiv.dataset.messageId = data.id || Date.now();
messageDiv.dataset.sender = data.from || currentUser?.id;
if (chatSettings.compactMode) {
messageDiv.style.gap = '8px';
}
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
const fallbackInitial = (data.fromUsername || data.sender || 'U').charAt(0).toUpperCase();
const shouldUseCurrentUserAvatar = String(data.from) === String(currentUser?.id) && currentUser?.avatar;
const avatarSource = data.fromAvatar || (shouldUseCurrentUserAvatar ? currentUser.avatar : null);
if (avatarSource && (String(avatarSource).startsWith('data:') || String(avatarSource).startsWith('http'))) {
avatar.style.backgroundImage = `url(${avatarSource})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
avatar.textContent = '';
} else {
avatar.style.backgroundImage = 'none';
avatar.textContent = fallbackInitial;
}
const content = document.createElement('div');
content.className = 'message-content';
const header = document.createElement('div');
header.className = 'message-header';
const username = document.createElement('span');
username.className = 'message-username';
username.textContent = data.fromUsername || data.sender;
header.appendChild(username);
const time = document.createElement('span');
time.className = 'message-time';
time.textContent = formatMessageTimestamp(data.timestamp);
time.title = formatMessageTimestamp(data.timestamp);
const text = document.createElement('div');
text.className = 'message-text';
if (chatSettings.fontSize === 'small') {
text.style.fontSize = '12px';
} else if (chatSettings.fontSize === 'large') {
text.style.fontSize = '16px';
}
if (data.mediaType === 'sticker') {
text.style.fontSize = '96px';
text.style.lineHeight = '1';
text.textContent = data.content;
} else {
const emojiRegex = /^[\p{Emoji}\p{Emoji_Component}\s]+$/u;
const isOnlyEmojis = data.content && emojiRegex.test(data.content.trim());
if (isOnlyEmojis && data.content.trim().length <= 20) {
text.style.fontSize = '64px';
text.style.lineHeight = '1.2';
}
text.textContent = data.content;
}
content.appendChild(header);
content.appendChild(text);
content.appendChild(time);
messageDiv.oncontextmenu = (e) => {
e.preventDefault();
showMessageContextMenu(e, messageDiv, data);
return false;
};
const actions = document.createElement('div');
actions.className = 'message-actions';
const reactBtn = document.createElement('button');
reactBtn.className = 'message-action-btn';
reactBtn.innerHTML = '👍';
reactBtn.title = 'React';
reactBtn.onclick = (e) => {
e.stopPropagation();
showQuickReactions(messageDiv.dataset.messageId, messageDiv, data);
};
const editBtn = document.createElement('button');
editBtn.className = 'message-action-btn';
editBtn.innerHTML = '✏️';
editBtn.title = 'Edit';
editBtn.onclick = (e) => {
e.stopPropagation();
editMessage(messageDiv.dataset.messageId, text);
};
const deleteBtn = document.createElement('button');
deleteBtn.className = 'message-action-btn';
deleteBtn.innerHTML = '🗑️';
deleteBtn.title = 'Delete';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteMessage(messageDiv.dataset.messageId, messageDiv);
};
const replyBtn = document.createElement('button');
replyBtn.className = 'message-action-btn';
replyBtn.innerHTML = '↩️';
replyBtn.title = 'Reply';
replyBtn.onclick = (e) => {
e.stopPropagation();
const input = document.getElementById('messageInput');
if (!input) return;
const previewText = (data.content || '').trim().slice(0, 40);
input.value = `@${data.fromUsername || 'user'} ${previewText ? `(${previewText}) ` : ''}`;
input.focus();
};
const pinChatBtn = document.createElement('button');
pinChatBtn.className = 'message-action-btn';
pinChatBtn.innerHTML = '📌';
pinChatBtn.title = 'Pin this chat';
pinChatBtn.onclick = (e) => {
e.stopPropagation();
togglePinCurrentChat();
};
actions.appendChild(reactBtn);
actions.appendChild(replyBtn);
actions.appendChild(pinChatBtn);
if (String(data.from) === String(currentUser?.id)) {
actions.appendChild(editBtn);
actions.appendChild(deleteBtn);
}
content.appendChild(actions);
messageDiv.addEventListener('click', (event) => {
if (event.target.closest('.message-actions') || event.target.closest('.message-context-menu') || event.target.closest('.reaction-picker')) {
return;
}
const wasOpen = messageDiv.classList.contains('message-actions-open');
closeAllMessageActionRows();
if (!wasOpen) {
messageDiv.classList.add('message-actions-open');
}
});
const reactionsDiv = document.createElement('div');
reactionsDiv.className = 'message-reactions';
reactionsDiv.dataset.messageId = messageDiv.dataset.messageId;
content.appendChild(reactionsDiv);
if (data.mediaType !== 'text' && data.mediaType !== 'sticker' && data.mediaUrl) {
const media = document.createElement('div');
media.className = 'message-media';
if (data.mediaType === 'image') {
const img = document.createElement('img');
img.src = data.mediaUrl;
img.alt = 'Image';
img.onerror = function() {
console.error('Failed to load image:', data.mediaUrl);
this.alt = '❌ Image failed to load';
this.style.display = 'none';
const errorMsg = document.createElement('div');
errorMsg.textContent = '❌ Image failed to load';
errorMsg.style.color = '#ff6b6b';
errorMsg.style.fontSize = '12px';
media.appendChild(errorMsg);
};
img.onload = function() {
console.log('✅ Image loaded successfully');
};
media.appendChild(img);
} else if (data.mediaType === 'video') {
const video = document.createElement('video');
video.src = data.mediaUrl;
video.controls = true;
video.preload = 'metadata';
video.playsInline = true;
video.setAttribute('playsinline', '');
video.setAttribute('webkit-playsinline', 'true');
video.onerror = function() {
console.error('Failed to load video:', data.mediaUrl);
const errorMsg = document.createElement('div');
errorMsg.textContent = '❌ Video failed to load';
errorMsg.style.color = '#ff6b6b';
errorMsg.style.fontSize = '12px';
media.appendChild(errorMsg);
};
video.onloadedmetadata = function() {
console.log('✅ Video loaded successfully');
};
media.appendChild(video);
} else if (data.mediaType === 'gif') {
const img = document.createElement('img');
img.src = data.mediaUrl;
img.style.maxWidth = '300px';
img.style.borderRadius = '8px';
img.alt = 'GIF';
media.appendChild(img);
} else if (data.mediaType === 'voice') {
const audio = document.createElement('audio');
audio.src = data.mediaUrl;
audio.controls = true;
audio.style.maxWidth = '300px';
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && audio.setSinkId) {
audio.setSinkId(audioSettings.speakerId).catch(err => {
console.warn('Could not set audio output device:', err);
});
}
media.appendChild(audio);
}
content.appendChild(media);
}
messageDiv.appendChild(avatar);
messageDiv.appendChild(content);
container.appendChild(messageDiv);
applyEnhancedEmoji(messageDiv);
setTimeout(() => {
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
refreshChatScrollbar();
}, 10);
if (chatSettings.disappearTime > 0) {
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.remove();
}
}, chatSettings.disappearTime * 1000);
}
}
function addCurrentChatToList() {
if (!currentChatContext || currentChatContext.type === 'none') {
showToast('Open a chat first', 'warning');
return;
}
const chatKey = `${currentChatContext.type}:${currentChatContext.id || currentChatContext.name}`;
if (!chatKey.includes(':') || chatKey.endsWith(':')) {
showToast('Open a valid chat first', 'warning');
return;
}
if (pinnedChats.includes(chatKey)) {
showToast('Chat already in your list', 'warning');
return;
}
pinnedChats.push(chatKey);
localStorage.setItem('pinnedChats', JSON.stringify(pinnedChats));
updatePinButtonState();
loadFriendsForDM();
showToast('Chat added to list');
}
function updateMembersList(users) {
const membersList = document.getElementById('membersList');
membersList.innerHTML = '';
getVisibleMembers(users).forEach(user => {
const item = document.createElement('div');
item.className = 'member-item';
item.style.cursor = 'pointer';
item.title = `Click to view ${user.username}'s profile`;
const avatar = document.createElement('div');
avatar.className = 'member-avatar';
if (user.avatar) {
avatar.style.backgroundImage = `url(${user.avatar})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
} else {
avatar.textContent = user.username ? user.username.charAt(0).toUpperCase() : 'U';
}
const status = document.createElement('div');
status.className = 'status-indicator';
const name = document.createElement('span');
name.textContent = user.username || 'Unknown';
const userId = user.id || user._id || user.userId;
const username = user.username || 'User';
item.dataset.userId = String(userId || '');
item.dataset.username = username;
item.onclick = () => {
if (String(userId) === FIRETECH_BOT_USER.userId) {
openDM(FIRETECH_BOT_USER.userId, FIRETECH_BOT_USER.username);
return;
}
if (!userId) {
showToast('❌ User ID not available', 'error');
return;
}
if (userId === currentUser.id || userId === currentUser._id) {
openProfile();
} else {
visitUserProfile(userId, username);
}
};
item.appendChild(avatar);
item.appendChild(status);
item.appendChild(name);
membersList.appendChild(item);
});
const mobileMembersList = document.getElementById('mobileMembersList');
if (mobileMembersList) {
mobileMembersList.innerHTML = membersList.innerHTML;
mobileMembersList.onclick = (e) => {
const memberItem = e.target.closest('.member-item');
if (!memberItem) return;
const mobileUserId = memberItem.dataset.userId;
const mobileUsername = memberItem.dataset.username || 'User';
if (!mobileUserId) {
showToast('❌ User ID not available', 'error');
return;
}
if (mobileUserId === String(currentUser.id) || mobileUserId === String(currentUser._id)) {
openProfile();
} else {
visitUserProfile(mobileUserId, mobileUsername);
}
};
}
}
function renderMembersSidebarFromRoom(members) {
const membersList = document.getElementById('membersList');
if (!membersList) return;
membersList.innerHTML = '';
const visibleMembers = Array.isArray(members) ? [...members] : [];
if (!visibleMembers.some((member) => String(member?.id || member?.userId || '') === FIRETECH_BOT_USER.userId)) {
visibleMembers.push(FIRETECH_BOT_USER);
}
visibleMembers.forEach((member) => {
const memberId = String(member.id || member.userId || '');
const isSelf = memberId && (memberId === String(currentUser?.id) || memberId === String(currentUser?._id));
const normalizedMember = {
...member,
id: memberId,
username: isSelf ? (currentUser?.username || member.username || 'You') : (member.username || 'User'),
avatar: isSelf ? (currentUser?.avatar || member.avatar || null) : (member.avatar || null)
};
const item = document.createElement('div');
item.className = 'member-item';
item.style.cursor = 'pointer';
const avatar = document.createElement('div');
avatar.className = 'member-avatar';
if (normalizedMember.avatar) {
avatar.style.backgroundImage = `url(${normalizedMember.avatar})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
} else {
avatar.textContent = String(normalizedMember.username || 'U').charAt(0).toUpperCase();
}
const status = document.createElement('div');
status.className = 'status-indicator';
const nameWrap = document.createElement('div');
nameWrap.style.display = 'flex';
nameWrap.style.flexDirection = 'column';
nameWrap.style.gap = '2px';
const name = document.createElement('span');
name.textContent = normalizedMember.username || 'Unknown';
name.style.fontWeight = '600';
const role = document.createElement('span');
role.textContent = normalizedMember.role || 'Member';
role.style.fontSize = '11px';
role.style.color = 'var(--text-secondary)';
nameWrap.appendChild(name);
nameWrap.appendChild(role);
const memberName = normalizedMember.username || 'User';
item.dataset.userId = String(memberId || '');
item.dataset.username = memberName;
item.onclick = () => {
if (!memberId) return;
if (memberId === FIRETECH_BOT_USER.userId) {
openDM(FIRETECH_BOT_USER.userId, FIRETECH_BOT_USER.username);
return;
}
if (String(memberId) === String(currentUser?.id) || String(memberId) === String(currentUser?._id)) {
openProfile();
} else {
visitUserProfile(memberId, memberName);
}
};
item.appendChild(avatar);
item.appendChild(status);
item.appendChild(nameWrap);
membersList.appendChild(item);
});
const mobileMembersList = document.getElementById('mobileMembersList');
if (mobileMembersList) {
mobileMembersList.innerHTML = membersList.innerHTML;
}
}
async function handleFileUpload(event) {
const files = Array.from(event.target.files || []);
if (files.length === 0) {
return;
}
const hasActiveChat = currentChatContext.type === 'dm' || currentChatContext.type === 'community' || currentChatContext.type === 'group';
if (!hasActiveChat) {
showToast('Open a DM, group, or community first, then select media', 'warning');
event.target.value = '';
return;
}
const inputArea = document.querySelector('.input-area');
let spinnerEl = null;
if (inputArea) {
spinnerEl = document.createElement('div');
spinnerEl.className = 'input-upload-spinner';
spinnerEl.innerHTML = '';
inputArea.insertBefore(spinnerEl, inputArea.firstChild);
}
const targetId = currentChatContext.type === 'dm' ? ensureChatTarget() : null;
let sentCount = 0;
const maxRawFileBytes = 24 * 1024 * 1024;
for (const file of files) {
if (file.size > maxRawFileBytes) {
showToast(`⚠️ ${file.name} is too large for in-chat upload. Keep each file under 24MB.`, 'warning');
continue;
}
const payload = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target.result;
// Support images, videos, and also treat webm/ogg/mp4 as reel videos
let mediaType = 'image';
const fileName = (file.name || '').toLowerCase();
const videoByExt = /\.(mp4|webm|mov|m4v|mkv|3gp|ogv|ogg)$/i.test(fileName);
const imageByExt = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(fileName);
if (String(file.type || '').startsWith('video') || videoByExt) mediaType = 'video';
else if (String(file.type || '').startsWith('image') || imageByExt) mediaType = 'image';
resolve({
content: file.name || (mediaType === 'image' ? 'Photo' : 'Video'),
mediaType,
mediaUrl: data
});
};
reader.onerror = () => resolve(null);
reader.readAsDataURL(file);
});
if (!payload) continue;
if (currentChatContext.type === 'community' || currentChatContext.type === 'group') {
sendRoomMessage(payload);
sentCount += 1;
} else if (targetId) {
await sendStagedDm(targetId, payload);
sentCount += 1;
}
}
if (spinnerEl) spinnerEl.remove();
if (sentCount > 0) {
showToast(`✅ Sent ${sentCount} media file(s)`, 'success');
}
event.target.value = '';
}
async function uploadProfileImage(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const avatar = e.target.result;
try {
const response = await fetch(`${API_BASE}/profile/upload`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ avatar })
});
const data = await response.json();
if (response.ok) {
currentUser.avatar = data.avatar;
loadUserProfile();
showToast('Profile image updated!');
} else {
showToast(data.error, 'error');
}
} catch (error) {
console.error(error);
showToast('Error uploading profile image', 'error');
}
};
reader.readAsDataURL(file);
event.target.value = '';
}
async function loadFriendsForDM() {
try {
const response = await fetch(`${API_BASE}/friends`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
const friends = await response.json();
const dict = getUiDictionary(currentLanguage);
const channelsList = document.getElementById('channelsList');
channelsList.innerHTML = '';
joinedCommunities = Array.from(new Set(joinedCommunities)).sort((a, b) => String(a).localeCompare(String(b)));
joinedGroups = Array.from(new Set(joinedGroups)).sort((a, b) => String(a).localeCompare(String(b)));
const pinnedFriends = [];
const unpinnedFriends = [];
friends.forEach(friend => {
const chatId = `dm:${friend.id}`;
if (pinnedChats.includes(chatId)) {
pinnedFriends.push(friend);
} else {
unpinnedFriends.push(friend);
}
});
if (pinnedFriends.length > 0) {
const pinnedTitle = document.createElement('div');
pinnedTitle.className = 'channel-section-title';
pinnedTitle.textContent = `📌 ${translateRawText('Pinned', dict)}`;
channelsList.appendChild(pinnedTitle);
pinnedFriends.forEach(friend => {
createFriendChannelItem(friend, channelsList, true);
});
}
if (joinedCommunities.length > 0) {
const communitiesTitle = document.createElement('div');
communitiesTitle.className = 'channel-section-title';
communitiesTitle.textContent = translateRawText('Communities', dict);
channelsList.appendChild(communitiesTitle);
joinedCommunities.forEach(community => {
const profile = getCommunityProfile(community);
const communityAvatar = profile.image
? ``
: `${profile.icon || '🌐'}
`;
const item = document.createElement('div');
const chatId = `community:${community}`;
item.className = 'channel-item';
if (pinnedChats.includes(chatId)) {
item.classList.add('pinned');
}
item.innerHTML = `
${communityAvatar}
${community}
${pinnedChats.includes(chatId) ? '' : ''}
`;
item.style.cursor = 'pointer';
item.onclick = () => openCommunityChat(community);
channelsList.appendChild(item);
});
}
if (joinedGroups.length > 0) {
const groupsTitle = document.createElement('div');
groupsTitle.className = 'channel-section-title';
groupsTitle.textContent = translateRawText('Groups', dict);
channelsList.appendChild(groupsTitle);
joinedGroups.forEach(group => {
const profile = getGroupProfile(group);
const groupAvatar = profile.image
? ``
: `${profile.icon || '👥'}
`;
const item = document.createElement('div');
const chatId = `group:${group}`;
item.className = 'channel-item';
if (pinnedChats.includes(chatId)) {
item.classList.add('pinned');
}
item.innerHTML = `
${groupAvatar}
${group}
${pinnedChats.includes(chatId) ? '' : ''}
`;
item.style.cursor = 'pointer';
item.onclick = () => openGroupChat(group);
channelsList.appendChild(item);
});
}
const directTitle = document.createElement('div');
directTitle.className = 'channel-section-title';
directTitle.textContent = translateRawText('Direct Messages', dict);
channelsList.appendChild(directTitle);
const selfItem = document.createElement('div');
selfItem.className = 'channel-item';
const selfAvatar = document.createElement('div');
selfAvatar.className = 'channel-avatar';
if (currentUser.avatar) {
selfAvatar.style.backgroundImage = `url(${currentUser.avatar})`;
selfAvatar.style.backgroundSize = 'cover';
selfAvatar.style.backgroundPosition = 'center';
} else {
selfAvatar.textContent = currentUser.username.charAt(0).toUpperCase();
}
const selfText = document.createElement('span');
selfText.className = 'channel-label';
selfText.textContent = `${currentUser.username} (${translateRawText('You', dict)})`;
selfItem.appendChild(selfAvatar);
selfItem.appendChild(selfText);
selfItem.onclick = () => openDM(currentUser.id, `${currentUser.username} (Notes)`);
channelsList.appendChild(selfItem);
if (unpinnedFriends.length === 0 && pinnedFriends.length === 0) {
const empty = document.createElement('div');
empty.style.padding = '10px';
empty.style.color = 'var(--text-secondary)';
empty.style.fontSize = '12px';
empty.textContent = translateRawText('No friends yet', dict);
channelsList.appendChild(empty);
} else {
unpinnedFriends.forEach(friend => {
createFriendChannelItem(friend, channelsList, false);
});
}
} catch (error) {
console.error('Failed to load friends for DM:', error);
}
}
function createFriendChannelItem(friend, container, isPinned) {
const item = document.createElement('div');
item.className = 'channel-item';
item.dataset.friendId = friend.id;
if (isPinned) {
item.classList.add('pinned');
}
const avatar = document.createElement('div');
avatar.className = 'channel-avatar';
if (friend.avatar) {
avatar.style.backgroundImage = `url(${friend.avatar})`;
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
} else {
avatar.textContent = friend.username.charAt(0).toUpperCase();
}
const text = document.createElement('span');
text.className = 'channel-label';
const status = friend.status === 'online' ? '🟢' : '⚫';
text.textContent = `${status} ${friend.username}`;
item.appendChild(avatar);
item.appendChild(text);
if (isPinned) {
const pinIndicator = document.createElement('span');
pinIndicator.className = 'pin-indicator';
pinIndicator.innerHTML = '';
item.appendChild(pinIndicator);
}
item.style.cursor = 'pointer';
item.onclick = () => openDM(friend.id, friend.username);
item.oncontextmenu = (e) => {
e.preventDefault();
showFriendContextMenu(e, friend.id, friend.username);
};
container.appendChild(item);
}
async function loadFriends() {
try {
const [requests, friends] = await Promise.all([
fetch(`${API_BASE}/friends/requests`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
}).then(r => r.json()),
fetch(`${API_BASE}/friends`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
}).then(r => r.json())
]);
const requestsDiv = document.getElementById('friendRequests');
requestsDiv.innerHTML = '';
const latestRequestsBySender = new Map();
requests.forEach((req) => {
const senderId = req?.from?.id ?? `request-${req?.id}`;
const prev = latestRequestsBySender.get(senderId);
if (!prev || Number(req.id) > Number(prev.id)) {
latestRequestsBySender.set(senderId, req);
}
});
const normalizedRequests = Array.from(latestRequestsBySender.values())
.sort((a, b) => Number(b.id) - Number(a.id));
if (normalizedRequests.length === 0) {
const emptyMsg = document.createElement('p');
emptyMsg.style.color = 'var(--text-secondary)';
emptyMsg.textContent = translateRawText('No pending requests', dict);
requestsDiv.appendChild(emptyMsg);
}
normalizedRequests.forEach((req) => {
const item = document.createElement('div');
item.className = 'friend-item';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
item.style.gap = '10px';
item.style.padding = '12px';
item.style.borderRadius = '10px';
item.style.border = '1px solid var(--border-color)';
item.style.background = 'rgba(255,255,255,0.02)';
item.innerHTML = `
${req.from.username}
${translateRawText('Pending request', dict)}
`;
requestsDiv.appendChild(item);
});
const friendsList = document.getElementById('friendsList');
friendsList.innerHTML = '';
if (friends.length === 0) {
const emptyMsg = document.createElement('p');
emptyMsg.style.color = 'var(--text-secondary)';
emptyMsg.textContent = translateRawText('No friends yet', dict);
friendsList.appendChild(emptyMsg);
}
friends.forEach(friend => {
const item = document.createElement('div');
item.className = 'friend-item';
item.style.cursor = 'pointer';
const status = friend.status === 'online'
? `🟢 ${translateRawText('Online', dict)}`
: `⚫ ${translateRawText('Offline', dict)}`;
item.innerHTML = `
${friend.username}
${status}
${translateRawText('Profile', dict)}
`;
friendsList.appendChild(item);
});
} catch (error) {
console.error('Failed to load friends:', error);
}
}
async function acceptFriendRequest(requestId) {
try {
const response = await fetch(`${API_BASE}/friends/accept`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ requestId })
});
const data = await response.json();
if (!response.ok) {
showToast(data.error || 'Failed to accept friend request', 'error');
return;
}
showToast('Friend request accepted!');
loadFriends();
loadFriendsForDM();
} catch (error) {
console.error(error);
showToast('Could not accept friend request', 'error');
}
}
async function sendFriendRequestFromLink(friendId) {
try {
const response = await fetch(`${API_BASE}/friends/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ toUserId: friendId })
});
if (response.ok) {
showToast('Friend request sent successfully!');
} else {
const data = await response.json();
showToast(data.error || 'Failed to send friend request', 'error');
}
} catch (error) {
console.error('Error sending friend request:', error);
showToast('Could not send friend request', 'error');
}
}
async function loadShopItems() {
try {
const syncedCoins = await syncCurrentUserCoinsFromProfile();
let userCoins = Number.isFinite(syncedCoins) ? syncedCoins : getCurrentCoinBalance();
const response = await fetch(`${API_BASE}/shop`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload?.error || 'Failed to load shop items');
}
const items = Array.isArray(payload) ? payload : [];
cachedShopItemsById = {};
items.forEach((item) => {
if (item?.id) cachedShopItemsById[item.id] = item;
if (item?.itemId) cachedShopItemsById[item.itemId] = item;
});
const shopList = document.getElementById('shopList');
shopList.innerHTML = '';
const dict = getUiDictionary(currentLanguage);
const hasKnownCoinBalance = Number.isFinite(userCoins);
const shopHeader = document.createElement('div');
shopHeader.className = 'shop-hero';
shopHeader.innerHTML = `
${translateRawText('Premium Shop', dict)}
${translateRawText('Instant equip items, profile cosmetics, and seasonal packs', dict)}
${translateRawText('Live Deals: up to 20% off featured items', dict)}
${translateRawText('You have', dict)}: ${hasKnownCoinBalance ? userCoins : '...'} 💰
`;
shopList.appendChild(shopHeader);
if (items.length === 0) {
const empty = document.createElement('p');
empty.style.color = 'var(--text-secondary)';
empty.textContent = translateRawText('No shop items available', dict);
shopList.appendChild(empty);
} else {
items.forEach(item => {
const itemDiv = document.createElement('div');
const parsedItemPrice = parseFlexibleNumber(item.price);
const itemPrice = Number.isFinite(parsedItemPrice) ? parsedItemPrice : 0;
const canAfford = Number.isFinite(userCoins) ? userCoins >= itemPrice : false;
const actionLabel = canAfford ? translateRawText('Buy Now', dict) : translateRawText('Need Coins', dict);
itemDiv.className = 'shop-item';
itemDiv.innerHTML = `
${item.name}
${item.category || 'Premium'}
${item.description}
${item.price} 💰
`;
if (hasKnownCoinBalance && !canAfford) {
itemDiv.style.opacity = '0.86';
itemDiv.title = `${translateRawText('Need', dict)} ${itemPrice} • ${translateRawText('You have', dict)} ${userCoins}`;
}
shopList.appendChild(itemDiv);
});
}
localizeStaticUi(currentLanguage);
} catch (error) {
console.error('Failed to load shop:', error);
showToast(error.message || 'Failed to load shop', 'error');
}
}
async function buyItem(itemId) {
try {
await syncCurrentUserCoinsFromProfile();
const response = await fetch(`${API_BASE}/shop/buy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ itemId })
});
const data = await response.json();
if (response.ok) {
if (typeof data?.coins === 'number') {
currentUser.coins = data.coins;
localStorage.setItem('user', JSON.stringify(currentUser));
updateAllCoinDisplays(data.coins);
}
const purchasedItem = data?.purchasedItem || cachedShopItemsById[itemId];
if (purchasedItem?.category) {
applyShopItem(purchasedItem.id || itemId, purchasedItem.category);
}
showToast('Item purchased and applied!', 'success');
loadShopItems();
loadInventory();
renderSavedItemsSummary();
} else {
await syncCurrentUserCoinsFromProfile();
loadShopItems();
loadInventory();
showToast(data?.error || 'Purchase failed', 'error');
}
} catch (error) {
console.error(error);
await syncCurrentUserCoinsFromProfile();
loadShopItems();
showToast('Error purchasing item', 'error');
}
}
async function loadInventory() {
try {
const response = await fetch(`${API_BASE}/inventory`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
const inventory = await response.json();
const inventoryList = document.getElementById('inventoryList');
inventoryList.innerHTML = '';
if (inventory.length === 0) {
inventoryList.innerHTML = 'No items yet
';
return;
}
inventory.forEach(item => {
const shopItem = item.ShopItem || item.itemId;
if (!shopItem) return;
const shopItemId = shopItem.id || shopItem.itemId;
const category = shopItem.category;
const isEquipped = Boolean(category && equippedShopItems?.[category] && String(equippedShopItems[category]) === String(shopItemId));
const itemDiv = document.createElement('div');
itemDiv.className = 'shop-item';
itemDiv.style.cursor = 'pointer';
const toggleButton = document.createElement('button');
toggleButton.className = 'buy-btn';
if (isEquipped) {
toggleButton.textContent = 'Disable';
toggleButton.style.background = 'var(--danger)';
toggleButton.onclick = () => toggleShopItem(shopItemId, category, false);
} else {
toggleButton.textContent = 'Enable';
toggleButton.style.background = 'var(--success)';
toggleButton.onclick = () => toggleShopItem(shopItemId, category, true);
}
itemDiv.innerHTML = `
${shopItem.name}
${shopItem.description}
Qty: ${item.quantity}
${isEquipped ? '✅ Enabled' : '⭕ Disabled'}
`;
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '6px';
buttonContainer.appendChild(toggleButton);
itemDiv.appendChild(buttonContainer);
inventoryList.appendChild(itemDiv);
});
renderSavedItemsSummary();
} catch (error) {
console.error('Failed to load inventory:', error);
}
}
function toggleShopItem(itemId, category, enable) {
if (enable) {
applyShopItem(itemId, category);
showToast('Item enabled!', 'success');
} else {
if (category) {
clearShopItemEffect(category);
delete equippedShopItems[category];
saveEquippedShopItems();
loadInventory();
showToast('Item disabled!', 'success');
}
}
}
function saveEquippedShopItems() {
localStorage.setItem('equippedShopItems', JSON.stringify(equippedShopItems));
}
function ensureThemeColorBackup() {
if (baseThemePrimaryColor && baseThemePrimaryHover) return;
const styles = getComputedStyle(document.documentElement);
baseThemePrimaryColor = baseThemePrimaryColor || styles.getPropertyValue('--primary-color').trim() || '#5865f2';
baseThemePrimaryHover = baseThemePrimaryHover || styles.getPropertyValue('--primary-hover').trim() || '#4752c4';
localStorage.setItem('baseThemePrimaryColor', baseThemePrimaryColor);
localStorage.setItem('baseThemePrimaryHover', baseThemePrimaryHover);
}
function clearShopItemEffect(category) {
if (!category) return;
if (category === 'Banners' || category === 'Themes') {
ensureThemeColorBackup();
document.documentElement.style.setProperty('--primary-color', baseThemePrimaryColor || '#5865f2');
document.documentElement.style.setProperty('--primary-hover', baseThemePrimaryHover || '#4752c4');
}
if (category === 'Effects') {
document.getElementById('message-glow-effect')?.remove();
}
if (category === 'Avatar') {
document.getElementById('avatar-aura-effect')?.remove();
}
if (category === 'Nameplates') {
document.getElementById('nameplate-effect')?.remove();
}
}
function unequipShopCategory(category) {
if (!category || !equippedShopItems?.[category]) {
showToast('Nothing equipped in this category', 'warning');
return;
}
clearShopItemEffect(category);
delete equippedShopItems[category];
saveEquippedShopItems();
loadInventory();
showToast('Item unequipped', 'success');
}
function applyPersistedShopEffects() {
if (!equippedShopItems || typeof equippedShopItems !== 'object') return;
Object.entries(equippedShopItems).forEach(([category, itemId]) => {
applyShopItem(itemId, category, { skipPersist: true, silent: true });
});
}
async function flushQueuedIceCandidates() {
if (!peerConnection || !remoteIceCandidatesQueue.length) return;
const queued = [...remoteIceCandidatesQueue];
remoteIceCandidatesQueue = [];
for (const candidate of queued) {
try {
await peerConnection.addIceCandidate(candidate);
} catch (error) {
console.warn('Failed to apply queued ICE candidate:', error);
}
}
}
async function addOrQueueIceCandidate(candidateData) {
if (!candidateData) return;
if (!peerConnection) {
pendingRemoteIceCandidates.push(candidateData);
return;
}
const candidate = new RTCIceCandidate(candidateData);
const hasRemoteDescription = Boolean(peerConnection.remoteDescription && peerConnection.remoteDescription.type);
if (hasRemoteDescription) {
try {
await peerConnection.addIceCandidate(candidate);
} catch (iceErr) {
console.warn('ICE candidate ignored:', iceErr.message);
}
} else {
remoteIceCandidatesQueue.push(candidate);
}
}
function applyShopItem(itemId, category, options = {}) {
const { skipPersist = false, silent = false } = options;
const notify = (message, type = 'success') => {
if (!silent) showToast(message, type);
};
const effectsMap = {
'Banners': () => {
ensureThemeColorBackup();
document.documentElement.style.setProperty('--primary-color', '#00d4ff');
document.documentElement.style.setProperty('--primary-hover', '#00b8e6');
notify('✨ Banner effect applied! Your profile now has a neon glow.', 'success');
},
'Badges': () => {
notify('🏆 Badge equipped! It now appears next to your name.', 'success');
},
'Themes': () => {
ensureThemeColorBackup();
const colors = ['#ff6b9d', '#c44569', '#4a69bd', '#6a89cc', '#60a3bc', '#78e08f', '#f6b93b', '#e55039'];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
document.documentElement.style.setProperty('--primary-color', randomColor);
document.documentElement.style.setProperty('--primary-hover', randomColor + 'dd');
notify('🎨 Theme applied! Your accent color has changed.', 'success');
},
'Effects': () => {
const style = document.createElement('style');
style.id = 'message-glow-effect';
style.textContent = `
.message[data-sender="${currentUser.id}"] .message-content {
animation: messageGlow 2s ease-in-out infinite;
}
@keyframes messageGlow {
0%, 100% { box-shadow: 0 0 5px rgba(88, 101, 242, 0.3); }
50% { box-shadow: 0 0 20px rgba(88, 101, 242, 0.6); }
}
`;
const existing = document.getElementById('message-glow-effect');
if (existing) existing.remove();
document.head.appendChild(style);
notify('✨ Message glow effect activated!', 'success');
},
'Avatar': () => {
const style = document.createElement('style');
style.id = 'avatar-aura-effect';
style.textContent = `
.message-avatar {
position: relative;
animation: avatarPulse 3s ease-in-out infinite;
}
@keyframes avatarPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(88, 101, 242, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(88, 101, 242, 0); }
}
`;
const existing = document.getElementById('avatar-aura-effect');
if (existing) existing.remove();
document.head.appendChild(style);
notify('🌟 Avatar aura effect activated!', 'success');
},
'Nameplates': () => {
const style = document.createElement('style');
style.id = 'nameplate-effect';
style.textContent = `
.message-username {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
`;
const existing = document.getElementById('nameplate-effect');
if (existing) existing.remove();
document.head.appendChild(style);
notify('💎 Crystal nameplate effect activated!', 'success');
},
'Emotes': () => {
notify('😎 Special emotes unlocked! Use them in your messages.', 'success');
}
};
const applyEffect = effectsMap[category];
if (applyEffect) {
clearShopItemEffect(category);
applyEffect();
if (!skipPersist && category) {
equippedShopItems[category] = itemId;
saveEquippedShopItems();
loadInventory();
}
} else {
notify('Effect applied!', 'success');
}
}
function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const dict = getUiDictionary(currentLanguage);
const sourceMessage = String(message || '');
let displayMessage = translateRawText(sourceMessage, dict);
const icon = type === 'success' ? '✓' : type === 'error' ? '✕' : '⚠';
const rawTitle = type === 'success' ? 'Success' : type === 'error' ? 'Error' : 'Warning';
const title = translateRawText(rawTitle, dict);
toast.innerHTML = `
${icon}
${title}
${displayMessage}
`;
container.appendChild(toast);
if (currentLanguage !== 'en' && displayMessage === sourceMessage && shouldAutoTranslate(sourceMessage)) {
fetchAutoTranslation(sourceMessage, currentLanguage).then((translated) => {
if (!translated) return;
const messageEl = toast.querySelector('.toast-message');
if (messageEl) messageEl.textContent = translated;
});
}
applyEnhancedEmoji(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
function openSettings() {
const panel = document.getElementById('settingsPanel');
if (!panel) return;
panel.classList.add('show');
if (currentUser) {
document.getElementById('settingsEmail').value = currentUser.email || '';
document.getElementById('settingsUsername').value = '';
}
populateAccountSwitcher();
applySettingsToUI();
switchSettingsTab('account', document.querySelector('.settings-nav-item'));
localizeStaticUi(currentLanguage);
}
function closeSettings() {
const panel = document.getElementById('settingsPanel');
if (!panel) return;
panel.classList.remove('show');
panel.removeAttribute('style');
}
function switchSettingsTab(tab, triggerEl) {
document.querySelectorAll('.settings-nav-item').forEach(item => {
item.classList.remove('active');
});
const navTrigger = triggerEl || (typeof event !== 'undefined' ? event.target : null);
if (navTrigger) {
navTrigger.classList.add('active');
}
document.querySelectorAll('.settings-section').forEach(section => {
section.classList.remove('active');
});
const targetSection = document.getElementById(tab + 'Settings');
if (targetSection) targetSection.classList.add('active');
if (tab === 'language') {
renderLangGrid();
const lang = LANGUAGES.find((l) => l.code === currentLanguage) || LANGUAGES[0];
const langDisplay = document.getElementById('currentLangDisplay');
if (langDisplay) langDisplay.textContent = `${lang.flag} ${lang.name}`;
}
}
function openLanguagePicker() {
openSettings();
const trigger = Array.from(document.querySelectorAll('.settings-nav-item')).find((item) => item.textContent.includes('Language'));
switchSettingsTab('language', trigger || null);
}
async function updateUsername() {
const newUsername = document.getElementById('settingsUsername').value.trim();
if (!newUsername) {
showToast('Please enter a new username', 'warning');
return;
}
try {
const response = await fetch(`${API_BASE}/profile`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ username: newUsername })
});
const data = await response.json();
if (response.ok) {
currentUser.username = newUsername;
localStorage.setItem('user', JSON.stringify(currentUser));
if (socket && socket.connected) {
socket.emit('update profile cache', {
username: newUsername,
avatar: currentUser.avatar || null
});
}
showToast('Username updated successfully!');
loadUserProfile();
document.getElementById('settingsUsername').value = '';
} else {
showToast(data.error || 'Failed to update username', 'error');
}
} catch (error) {
console.error(error);
showToast('Error updating username', 'error');
}
}
function changeTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
showToast(`Switched to ${theme} theme`, 'success');
}
function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
const themeSelector = document.getElementById('themeSelector');
if (themeSelector) {
themeSelector.value = savedTheme;
}
}
function closeCurrentChat() {
if (activeRoomSubscription && socket && socket.connected) {
socket.emit('leave room chat', activeRoomSubscription);
}
activeRoomSubscription = null;
currentChatFriendId = null;
currentChatFriendName = null;
currentChatContext = { type: 'none', id: null, name: null };
clearPendingMediaDraft();
const chatTitleEl = document.getElementById('chatTitle');
chatTitleEl.textContent = '💬 Select a friend to chat';
chatTitleEl.onclick = null;
document.getElementById('messages-container').innerHTML = '';
document.getElementById('closeChatBtn').style.display = 'none';
refreshChatScrollbar();
showToast('Chat closed');
}
async function clearAndCloseCurrentChat() {
if (!currentChatContext || currentChatContext.type === 'none') {
showToast('No active chat to clear', 'warning');
return;
}
const cleared = await clearCurrentChatHistory();
if (cleared) {
closeCurrentChat();
}
}
function getCurrentRoomKey() {
if (currentChatContext.type === 'community') return normalizeRoomKeyClient('community', currentChatContext.name);
if (currentChatContext.type === 'group') return normalizeRoomKeyClient('group', currentChatContext.name);
return null;
}
function saveRoomMessages() {
const buildCompactCache = (maxPerRoom, keepMedia = true) => {
const compact = {};
for (const [roomKey, msgs] of Object.entries(roomMessages || {})) {
const latest = Array.isArray(msgs) ? msgs.slice(-maxPerRoom) : [];
compact[roomKey] = latest.map((m) => {
const copy = {
messageId: m?.messageId || m?.id || null,
from: m?.from || null,
fromUsername: m?.fromUsername || 'User',
fromAvatar: m?.fromAvatar || null,
content: m?.content || '',
mediaType: m?.mediaType || 'text',
roomType: m?.roomType,
roomName: m?.roomName,
timestamp: m?.timestamp || new Date().toISOString()
};
const mediaUrl = typeof m?.mediaUrl === 'string' ? m.mediaUrl : '';
if (keepMedia && mediaUrl && mediaUrl.length <= ROOM_CACHE_MEDIA_URL_MAX_CHARS) {
copy.mediaUrl = mediaUrl;
} else if (copy.mediaType !== 'text') {
copy.mediaUrl = null;
if (!copy.content) copy.content = `${copy.mediaType} message`;
}
return copy;
});
}
return compact;
};
const attempts = [
() => buildCompactCache(ROOM_CACHE_MAX_PER_ROOM, true),
() => buildCompactCache(24, false),
() => buildCompactCache(12, false),
() => buildCompactCache(6, false)
];
for (const build of attempts) {
try {
localStorage.setItem('roomMessages', JSON.stringify(build()));
return;
} catch (error) {
if (error?.name !== 'QuotaExceededError') {
console.warn('Failed saving room messages cache:', error);
return;
}
}
}
try {
localStorage.removeItem('roomMessages');
} catch (_) {}
}
function addRoomMessage(roomKey, message) {
if (!roomMessages[roomKey]) roomMessages[roomKey] = [];
roomMessages[roomKey].push(message);
saveRoomMessages();
}
function renderRoomMessages(roomKey, roomName, roomType) {
const container = document.getElementById('messages-container');
container.innerHTML = '';
const messages = roomMessages[roomKey] || [];
if (messages.length === 0) {
const welcome = document.createElement('div');
welcome.className = 'message';
welcome.innerHTML = `
${roomType === 'community' ? '🌐' : '👥'}
Welcome to ${roomName}. Messages sent here stay in this ${roomType}.
`;
container.appendChild(welcome);
refreshChatScrollbar();
return;
}
messages.forEach(msg => addMessageToChat(msg));
refreshChatScrollbar();
}
async function sendStagedDm(targetId, payload) {
if (!socket || !socket.connected) {
showToast('Connection lost. Reconnecting...', 'warning');
connectSocket();
throw new Error('Socket not connected');
}
const optimistic = {
id: Date.now(),
from: currentUser.id,
fromUsername: currentUser.username,
fromAvatar: currentUser.avatar,
to: targetId,
content: payload.content,
mediaType: payload.mediaType,
mediaUrl: payload.mediaUrl || null,
timestamp: new Date().toISOString()
};
if (currentChatContext.type === 'dm' && String(currentChatContext.id) === String(targetId)) {
addMessageToChat(optimistic);
}
socket.emit('send dm', {
toUserId: targetId,
content: payload.content,
mediaType: payload.mediaType,
mediaUrl: payload.mediaUrl || null
});
}
function sendRoomMessage(payload) {
const roomKey = getCurrentRoomKey();
if (!roomKey) {
showToast('Open a community/group chat first', 'warning');
return;
}
if (!socket || !socket.connected) {
showToast('Connection lost. Reconnecting...', 'warning');
connectSocket();
return;
}
socket.emit('send room message', {
roomType: currentChatContext.type,
roomName: currentChatContext.name,
content: payload.content,
mediaType: payload.mediaType,
mediaUrl: payload.mediaUrl || null
});
}
function ensureChatTarget() {
if (currentChatContext.type === 'community' || currentChatContext.type === 'group') {
return null;
}
if (currentChatFriendId) {
return currentChatFriendId;
}
if (currentUser && currentUser.id) {
openDM(currentUser.id, `${currentUser.username} (Notes)`);
return currentUser.id;
}
return null;
}
function requestNotificationPermission() {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
showToast('Desktop notifications enabled!');
document.getElementById('desktopNotifications').checked = true;
}
});
}
}
function showDesktopNotification(title, message, isSender = false) {
const desktopNotifEnabled = document.getElementById('desktopNotifications')?.checked;
if (!desktopNotifEnabled || isSender) return;
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, {
body: message,
icon: '/favicon.ico',
badge: '/favicon.ico'
});
}
}
// ==================== IN-APP NOTIFICATION FEED ====================
function pushNotification(title, body, action = null) {
const notif = {
id: Date.now(),
title,
body,
action,
time: new Date().toISOString(),
read: false
};
notifList.unshift(notif);
if (notifList.length > 100) notifList.length = 100;
localStorage.setItem('notifList', JSON.stringify(notifList));
notifUnreadCount++;
updateNotifBadge();
}
function updateNotifBadge() {
const badge = document.getElementById('notifBadge');
if (!badge) return;
if (notifUnreadCount > 0) {
badge.textContent = notifUnreadCount > 99 ? '99+' : notifUnreadCount;
badge.classList.add('visible');
} else {
badge.classList.remove('visible');
}
}
function openNotifPanel() {
const panel = document.getElementById('notifPanel');
if (!panel) return;
panel.classList.add('show');
// Mark all as read
notifList.forEach(n => n.read = true);
localStorage.setItem('notifList', JSON.stringify(notifList));
notifUnreadCount = 0;
updateNotifBadge();
renderNotifList();
}
function closeNotifPanel() {
document.getElementById('notifPanel')?.classList.remove('show');
}
function clearAllNotifications() {
notifList = [];
localStorage.setItem('notifList', JSON.stringify(notifList));
notifUnreadCount = 0;
updateNotifBadge();
renderNotifList();
}
function renderNotifList() {
const el = document.getElementById('notifList');
if (!el) return;
if (notifList.length === 0) {
el.innerHTML = 'No notifications yet
';
return;
}
el.innerHTML = notifList.map(n => {
const timeStr = n.time ? new Date(n.time).toLocaleString() : '';
const unreadClass = n.read ? '' : 'notif-unread';
const actionAttr = n.action ? `onclick="handleNotifAction(${JSON.stringify(JSON.stringify(n.action))})"` : '';
return `
${n.title}
${n.body}
${timeStr}
`;
}).join('');
}
function handleNotifAction(actionStr) {
try {
const action = JSON.parse(actionStr);
closeNotifPanel();
if (action.type === 'dm') {
openDM(action.friendId, action.friendName);
} else if (action.type === 'community' || action.type === 'group') {
if (action.type === 'community') openCommunityChat(action.name);
else openGroupChat(action.name);
}
} catch(e) {}
}
function playNotificationSound() {
if (!chatSettings.messageSound) return;
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.08);
} catch (error) {
console.error('Sound notification failed:', error);
}
}
function saveNotificationSettings() {
chatSettings.notificationMode = document.getElementById('notificationMode')?.value || 'all';
chatSettings.notifyOnSent = document.getElementById('notifyOnSent')?.checked || false;
chatSettings.notifyOnReceived = document.getElementById('notifyOnReceived')?.checked ?? true;
chatSettings.desktopNotifications = document.getElementById('desktopNotifications')?.checked || false;
chatSettings.messageSound = document.getElementById('messageSound')?.checked ?? true;
localStorage.setItem('chatSettings', JSON.stringify(chatSettings));
if (chatSettings.desktopNotifications) {
requestNotificationPermission();
}
}
function saveChatSettings() {
chatSettings.disappearTime = Number(document.getElementById('disappearTime')?.value || 0);
chatSettings.enterToSend = document.getElementById('enterToSend')?.checked ?? true;
chatSettings.showTypingIndicator = document.getElementById('showTypingIndicator')?.checked ?? true;
chatSettings.autoplayGifs = document.getElementById('autoplayGifs')?.checked ?? true;
localStorage.setItem('chatSettings', JSON.stringify(chatSettings));
}
function saveAppearanceSettings() {
chatSettings.compactMode = document.getElementById('compactMode')?.checked || false;
chatSettings.fontSize = document.getElementById('fontSizeSelector')?.value || 'medium';
localStorage.setItem('chatSettings', JSON.stringify(chatSettings));
}
function applySettingsToUI() {
const savedSettings = JSON.parse(localStorage.getItem('chatSettings') || '{}');
chatSettings = { ...chatSettings, ...savedSettings };
const setChecked = (id, value) => {
const el = document.getElementById(id);
if (el) el.checked = !!value;
};
const setValue = (id, value) => {
const el = document.getElementById(id);
if (el) el.value = value;
};
setValue('disappearTime', String(chatSettings.disappearTime));
setChecked('enterToSend', chatSettings.enterToSend);
setChecked('showTypingIndicator', chatSettings.showTypingIndicator);
setChecked('autoplayGifs', chatSettings.autoplayGifs);
setChecked('desktopNotifications', chatSettings.desktopNotifications);
setChecked('messageSound', chatSettings.messageSound);
setChecked('notifyOnSent', chatSettings.notifyOnSent);
setChecked('notifyOnReceived', chatSettings.notifyOnReceived);
setChecked('compactMode', chatSettings.compactMode);
setValue('fontSizeSelector', chatSettings.fontSize);
setValue('notificationMode', chatSettings.notificationMode);
}
function openCommunitiesModal() {
const modal = document.getElementById('communitiesModal');
const list = document.getElementById('communitiesList');
const dict = getUiDictionary(currentLanguage);
const communities = [
{ name: 'Gaming', icon: '🕹️', description: 'Talk about your favorite games' },
{ name: 'Coding', icon: '🧠', description: 'Share code and learn together' },
{ name: 'Music', icon: '🎙️', description: 'Discuss and share music' },
{ name: 'Art', icon: '🖌️', description: 'Share your creative works' },
{ name: 'Sports', icon: '🏟️', description: 'Sports fans unite' },
{ name: 'Movies', icon: '🎞️', description: 'Film enthusiasts' }
];
list.innerHTML = communities.map(c => {
const isJoined = joinedCommunities.includes(c.name);
const profile = getCommunityProfile(c.name);
const safeCommunityName = String(c.name).replace(/'/g, "\\'");
const avatarHtml = profile.image
? ``
: `${profile.icon || c.icon}
`;
return `
`;
}).join('');
modal.classList.remove('hidden');
localizeStaticUi(currentLanguage);
}
function closeCommunitiesModal() {
document.getElementById('communitiesModal').classList.add('hidden');
}
function filterCommunitiesList(query) {
const items = document.querySelectorAll('#communitiesList .community-card');
const q = String(query || '').toLowerCase();
items.forEach((card) => {
const name = card.querySelector('.group-name')?.textContent?.toLowerCase() || '';
card.style.display = name.includes(q) ? '' : 'none';
});
}
function openGroupsModal() {
const modal = document.getElementById('groupsModal');
const list = document.getElementById('groupsList');
const dict = getUiDictionary(currentLanguage);
const baseGroups = [
{ name: 'Study Group', emoji: '🧠', description: 'Study together and share notes' },
{ name: 'Project Team', emoji: '🧰', description: 'Collaborate on projects' },
{ name: 'Family', emoji: '🫶', description: 'Family group chat' },
{ name: 'Friends', emoji: '🎈', description: 'Hang out with friends' },
{ name: 'Work', emoji: '🏙️', description: 'Work related discussions' },
{ name: 'Gaming', emoji: '🕹️', description: 'Gaming sessions and talk' },
{ name: 'Music', emoji: '🎙️', description: 'Share music and playlists' }
];
const groups = Array.from(new Set([...baseGroups.map((g) => g.name), ...joinedGroups])).map((groupName) => {
const preset = baseGroups.find((g) => g.name === groupName) || { name: groupName, emoji: '👥', description: 'Custom group chat' };
const roles = groupMemberRoles[groupName] || {};
const dynamicCount = Math.max(1, Object.keys(roles).length || (joinedGroups.includes(groupName) ? 1 : 0));
return { ...preset, members: dynamicCount };
});
list.innerHTML = groups.map(g => {
const isJoined = joinedGroups.includes(g.name);
const profile = getGroupProfile(g.name);
const safeGroupName = String(g.name).replace(/'/g, "\\'");
const avatarHtml = profile.image
? ``
: `${profile.icon || g.emoji}
`;
return `
${avatarHtml}
${g.name}
${g.description}
👥 ${g.members} members
${isJoined ? `
` : ''}
`;
}).join('');
modal.classList.remove('hidden');
localizeStaticUi(currentLanguage);
}
function closeGroupsModal() {
document.getElementById('groupsModal').classList.add('hidden');
}
function openGroupAppearanceEditor(name) {
const profile = getGroupProfile(name);
const safeName = String(name).replace(/'/g, "\\'");
const canManage = canManageRoomAppearance('group', name);
const iconList = ['🧠', '⚡', '🔐', '🌐', '📡', '🎯', '🚀', '💎', '🏆', '🎨', '🔧', '📊', '🎭', '💼', '📚', '🎬', '🎮', '🏃', '🌟', '🔥', '❤️', '🎉', '⭐', '🎪', '🎵', '🎸', '🎤', '🎲', '🧩', '🎓'];
const iconsHtml = iconList.map((icon) => (
``
)).join('');
const html = `
Customize group appearance for ${name}. ${canManage ? 'You can manage this room.' : 'Only Owner or Admin can edit this room.'}
${iconsHtml}
`;
customAlert(html, `🎨 ${name} Appearance`, '🎨');
}
function renameGroupPrompt(name) {
if (!canManageRoomAppearance('group', name)) {
showToast('Only Owner or Admin can rename this group', 'warning');
return;
}
customPrompt('Enter new group name', '✏ Rename Group', name, name, '✏').then((newName) => {
const trimmed = String(newName || '').trim();
if (!trimmed || trimmed === name) return;
const idx = joinedGroups.indexOf(name);
if (idx >= 0) {
joinedGroups[idx] = trimmed;
localStorage.setItem('joinedGroups', JSON.stringify(joinedGroups));
}
if (groupProfiles[name]) {
groupProfiles[trimmed] = { ...groupProfiles[name], name: trimmed };
delete groupProfiles[name];
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
}
if (groupMemberRoles[name]) {
groupMemberRoles[trimmed] = groupMemberRoles[name];
delete groupMemberRoles[name];
localStorage.setItem('groupMemberRoles', JSON.stringify(groupMemberRoles));
}
const oldKey = normalizeRoomKeyClient('group', name);
const newKey = normalizeRoomKeyClient('group', trimmed);
if (roomMessages[oldKey]) {
roomMessages[newKey] = roomMessages[oldKey];
delete roomMessages[oldKey];
saveRoomMessages();
}
if (currentChatContext.type === 'group' && currentChatContext.name === name) {
openGroupChat(trimmed);
}
emitRoomProfileUpdate('group', name, { name: trimmed });
loadFriendsForDM();
showToast('Group renamed successfully', 'success');
});
}
function showGroupRoles(name) {
const roleMap = groupMemberRoles[name] || {};
const activeMembers = Array.isArray(currentRoomMembers) && currentChatContext?.type === 'group' && currentChatContext?.name === name
? currentRoomMembers
: [];
const membersById = new Map();
activeMembers.forEach((m) => {
const memberId = String(m?.id || m?.userId || '');
if (!memberId) return;
membersById.set(memberId, {
id: memberId,
username: m?.username || 'User',
role: m?.role || roleMap[memberId] || 'Member'
});
});
Object.entries(roleMap).forEach(([memberId, role]) => {
if (membersById.has(memberId)) return;
const onlineMatch = (globalOnlineUsers || []).find((u) => String(u?.id || u?.userId || u?._id || '') === String(memberId));
membersById.set(memberId, {
id: String(memberId),
username: onlineMatch?.username || `User ${String(memberId).slice(0, 6)}`,
role: role || 'Member'
});
});
const members = Array.from(membersById.values());
const myRole = roleMap[String(currentUser?.id || '')] || 'Member';
const canManage = myRole === 'Owner' || myRole === 'Admin';
const rows = members.map((m) => {
const memberId = String(m.id || m.userId || '');
const role = m.role || roleMap[memberId] || 'Member';
const self = memberId === String(currentUser?.id || '');
const controls = canManage && !self
? `
`
: `${role}
`;
return `
${m.username || 'User'}
${role}${self ? ' (You)' : ''}
${controls}
`;
}).join('');
customAlert(`
Backend-synced roles for active group room. Your role: ${myRole}.
${rows || '
Open the group chat first to load live members.
'}
`, `👥 ${name} Roles`, '👥');
}
function assignGroupRole(groupName, targetUserId, role) {
if (!socket || !socket.connected) {
showToast('Not connected to server', 'warning');
return;
}
socket.emit('set room role', {
roomType: 'group',
roomName: groupName,
targetUserId,
role
});
showToast(`Requested role change to ${role}`, 'success');
}
function setGroupIcon(name, icon) {
if (!canManageRoomAppearance('group', name)) {
showToast('Only Owner or Admin can change group icon', 'warning');
return;
}
const profile = getGroupProfile(name);
profile.icon = icon;
profile.image = '';
groupProfiles[name] = profile;
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
cacheRoomProfile('group', name, { icon, image: '' });
closeCustomDialog();
emitRoomProfileUpdate('group', name, { icon, image: '' });
refreshRoomVisuals('group', name, { reopenDetails: true });
showToast('Group icon updated!', 'success');
}
function resetGroupAppearance(name) {
if (!canManageRoomAppearance('group', name)) {
showToast('Only Owner or Admin can reset group appearance', 'warning');
return;
}
const profile = getGroupProfile(name);
profile.image = '';
profile.icon = '👥';
groupProfiles[name] = profile;
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
cacheRoomProfile('group', name, { icon: '👥', image: '' });
closeCustomDialog();
emitRoomProfileUpdate('group', name, { icon: '👥', image: '' });
refreshRoomVisuals('group', name, { reopenDetails: true });
showToast('Group appearance reset', 'success');
}
function subscribeToRoomChat(roomType, roomName) {
if (!socket || !socket.connected) return;
if (activeRoomSubscription) {
socket.emit('leave room chat', activeRoomSubscription);
}
activeRoomSubscription = { roomType, roomName };
socket.emit('join room chat', { roomType, roomName });
}
function openCommunityChat(name) {
if (name && typeof name === 'object') {
name = name.name || name.id || String(name);
}
name = String(name || 'Community');
console.log('🌐 Opening community chat:', name);
const profile = getCommunityProfile(name);
const dict = getUiDictionary(currentLanguage);
const communityLabel = translateRawText('Community', dict);
currentChatFriendId = null;
currentChatFriendName = `${communityLabel}: ${name}`;
currentChatContext = { type: 'community', id: name, name };
clearPendingMediaDraft();
updatePinButtonState();
const chatTitleEl = document.getElementById('chatTitle');
chatTitleEl.textContent = `${profile?.image ? '🖼️' : (profile.icon || '🌐')} ${communityLabel}: ${name}`;
chatTitleEl.onclick = () => openCommunityDetails(name);
renderRoomMessages(normalizeRoomKeyClient('community', name), name, 'community');
currentRoomMembers = [{
id: currentUser?.id,
username: currentUser?.username || 'You',
avatar: currentUser?.avatar || null,
role: getMyRoomRole('community', name) || 'Owner'
}];
renderMembersSidebarFromRoom(currentRoomMembers);
subscribeToRoomChat('community', name);
document.getElementById('closeChatBtn').style.display = 'block';
showSection('chat');
if (window.innerWidth <= 480) {
closeMobileSidebar();
}
}
function openGroupChat(name) {
if (name && typeof name === 'object') {
name = name.name || name.id || String(name);
}
name = String(name || 'Group');
console.log('👥 Opening group chat:', name);
const profile = getGroupProfile(name);
const dict = getUiDictionary(currentLanguage);
const groupLabel = translateRawText('Group', dict);
currentChatFriendId = null;
currentChatFriendName = `${groupLabel}: ${name}`;
currentChatContext = { type: 'group', id: name, name };
clearPendingMediaDraft();
updatePinButtonState();
const chatTitleEl = document.getElementById('chatTitle');
chatTitleEl.textContent = `${profile?.image ? '🖼️' : (profile.icon || '👥')} ${groupLabel}: ${name}`;
chatTitleEl.onclick = null;
renderRoomMessages(normalizeRoomKeyClient('group', name), name, 'group');
currentRoomMembers = [{
id: currentUser?.id,
username: currentUser?.username || 'You',
avatar: currentUser?.avatar || null,
role: getMyRoomRole('group', name) || 'Owner'
}];
renderMembersSidebarFromRoom(currentRoomMembers);
subscribeToRoomChat('group', name);
document.getElementById('closeChatBtn').style.display = 'block';
showSection('chat');
if (window.innerWidth <= 480) {
closeMobileSidebar();
}
}
function joinCommunity(name) {
if (joinedCommunities.includes(name)) {
showToast('Already joined this community', 'warning');
return;
}
const profile = getCommunityProfile(name);
const alreadyMember = (profile.members || []).some((m) => String(m.id) === String(currentUser?.id));
if (!alreadyMember) {
profile.members = profile.members || [];
profile.members.push({
id: currentUser?.id,
username: currentUser?.username || 'You',
role: profile.members.length === 0 ? 'leader' : 'member'
});
communityProfiles[name] = profile;
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
}
joinedCommunities.push(name);
localStorage.setItem('joinedCommunities', JSON.stringify(joinedCommunities));
loadFriendsForDM();
showToast(`Joined ${name} community!`);
}
function joinGroup(name) {
if (joinedGroups.includes(name)) {
showToast('Already joined this group', 'warning');
return;
}
getGroupProfile(name);
if (!groupMemberRoles[name]) {
groupMemberRoles[name] = {};
}
const currentUserId = String(currentUser?.id || 'self');
const existingRoles = groupMemberRoles[name];
if (!existingRoles[currentUserId]) {
existingRoles[currentUserId] = Object.keys(existingRoles).length === 0 ? 'Owner' : 'Member';
}
localStorage.setItem('groupMemberRoles', JSON.stringify(groupMemberRoles));
joinedGroups.push(name);
localStorage.setItem('joinedGroups', JSON.stringify(joinedGroups));
loadFriendsForDM();
showToast(`Joined ${name} group!`);
}
const stickers = [
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃',
'😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙',
'😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔',
'🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥',
'😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮',
'🤧', '🥵', '🥶', '😵', '🤯', '🤠', '🥳', '😎', '🤓', '🧐',
'👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉',
'👆', '👇', '☝️', '✋', '🤚', '🖐', '🖖', '👋', '🤙', '💪',
'❤️', '🧡', '💛', '💚', '💙', '💜', '🤎', '🖤', '🤍', '💔',
'❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️',
'✝️', '☪️', '🕉', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '⚙️',
'🔥', '💯', '✨', '🌟', '⭐', '💫', '🌈', '☀️', '🌙', '⚡',
'☁️', '🌊', '🌺', '🌸', '🌼', '🌻', '🌷', '🌹', '🥀', '🌴'
];
const GIF_LIBRARY_BY_CATEGORY = {
Trending: [
'https://media.giphy.com/media/3oEjI6SIIHBdRxXI40/giphy.gif',
'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif',
'https://media.giphy.com/media/l0MYt5jPR6QX5pnqM/giphy.gif',
'https://media.giphy.com/media/111ebonMs90YLu/giphy.gif',
'https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif',
'https://media.giphy.com/media/3o7527pa7qs9kCG78A/giphy.gif',
'https://media.giphy.com/media/XreQmk7ETCak0/giphy.gif',
'https://media.giphy.com/media/l0HlNQ03J5JxX6lva/giphy.gif'
],
Reactions: [
'https://media.giphy.com/media/26tknCqiJrBQG6bxC/giphy.gif',
'https://media.giphy.com/media/BPJmthQ3YRwD6QqcVD/giphy.gif',
'https://media.giphy.com/media/g9582DNuQppxC/giphy.gif',
'https://media.giphy.com/media/VbnUQpnihPSIgIXuZv/giphy.gif',
'https://media.giphy.com/media/l3q2K5jinAlChoCLS/giphy.gif',
'https://media.giphy.com/media/H3nnOlR7yiCXXQ8e2I/giphy.gif',
'https://media.giphy.com/media/LSKVmdIwZFeNEBKBxZ/giphy.gif',
'https://media.giphy.com/media/aWPGuTlDqq2yc/giphy.gif'
],
Celebration: [
'https://media.giphy.com/media/3oz8xZvvOZRmKay4xy/giphy.gif',
'https://media.giphy.com/media/YRuFixSNWFVcXaxpmX/giphy.gif',
'https://media.giphy.com/media/3o7qDEq2bMbcbPRQ2c/giphy.gif',
'https://media.giphy.com/media/kyLYXonQYYfwYDIeZl/giphy.gif',
'https://media.giphy.com/media/l1J3CbFgn5o7DGRuE/giphy.gif',
'https://media.giphy.com/media/TdfyKrN7HGTIY/giphy.gif',
'https://media.giphy.com/media/3o7aCRloybJlXpNjSU/giphy.gif',
'https://media.giphy.com/media/8lQyyys3SGBoUUxrUp/giphy.gif'
],
Gaming: [
'https://media.giphy.com/media/l4FGuhL4U2WyjdkaY/giphy.gif',
'https://media.giphy.com/media/xTiTnqUxyWbsAXq7Ju/giphy.gif',
'https://media.giphy.com/media/5GoVLqeAOo6PK/giphy.gif',
'https://media.giphy.com/media/3og0IPxMM0erATueVW/giphy.gif',
'https://media.giphy.com/media/10LKovKon8DENq/giphy.gif',
'https://media.giphy.com/media/3o7TKU8RvQuomFfUUU/giphy.gif',
'https://media.giphy.com/media/9J7tdYltWyXIY/giphy.gif',
'https://media.giphy.com/media/26gssIytJvy1b1THO/giphy.gif'
]
};
let currentPickerTab = 'stickers';
function togglePicker() {
const picker = document.getElementById('pickerPanel');
if (!picker) return;
picker.classList.toggle('show');
if (picker.classList.contains('show')) {
applyPickerScale();
loadPickerContent();
}
}
function switchPickerTab(tab, triggerEl) {
currentPickerTab = tab;
document.querySelectorAll('.picker-tab').forEach(t => t.classList.remove('active'));
const tabTrigger = triggerEl || (typeof event !== 'undefined' ? event.target : null);
if (tabTrigger) tabTrigger.classList.add('active');
loadPickerContent();
}
function loadPickerContent() {
const content = document.getElementById('pickerContent');
content.innerHTML = '';
content.classList.toggle('gifs-mode', currentPickerTab === 'gifs');
const dict = getUiDictionary(currentLanguage);
if (currentPickerTab === 'stickers') {
stickers.forEach(sticker => {
const item = document.createElement('div');
item.className = 'sticker-item';
item.innerHTML = `${sticker}`;
item.onclick = () => sendSticker(sticker);
content.appendChild(item);
});
applyEnhancedEmoji(content);
} else if (currentPickerTab === 'gifs') {
Object.entries(GIF_LIBRARY_BY_CATEGORY).forEach(([category, gifs]) => {
const section = document.createElement('div');
section.style.display = 'grid';
section.style.gap = '10px';
section.style.marginBottom = '16px';
section.style.gridColumn = '1 / -1';
const heading = document.createElement('div');
heading.textContent = translateRawText(category, dict);
heading.style.fontSize = '13px';
heading.style.fontWeight = '700';
heading.style.color = 'var(--text-secondary)';
heading.style.letterSpacing = '0.5px';
section.appendChild(heading);
const grid = document.createElement('div');
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(2, 1fr)';
grid.style.gap = '12px';
gifs.forEach(gifUrl => {
const img = document.createElement('img');
img.className = 'gif-item';
img.src = gifUrl;
img.loading = 'lazy';
img.onclick = () => sendGif(gifUrl);
grid.appendChild(img);
});
section.appendChild(grid);
content.appendChild(section);
});
}
localizeStaticUi(currentLanguage);
}
function sendSticker(sticker) {
const messageInput = document.getElementById('messageInput');
if (messageInput) {
const currentText = messageInput.value;
messageInput.value = currentText + sticker;
messageInput.focus();
}
document.getElementById('pickerPanel').classList.remove('show');
}
function sendGif(gifUrl) {
setPendingMediaDraft({
mediaType: 'gif',
mediaUrl: gifUrl,
content: 'GIF'
});
showToast('GIF selected. Press Send to post.', 'success');
document.getElementById('pickerPanel').classList.remove('show');
}
let mediaRecorder;
let audioChunks = [];
let recordingInterval;
let recordingSeconds = 0;
let recordedAudioMimeType = 'audio/webm';
function getSupportedAudioMimeType() {
const candidates = ['audio/mp4', 'audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus'];
for (const type of candidates) {
if (window.MediaRecorder && MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return '';
}
function toggleVoiceRecorder() {
const recorder = document.getElementById('voiceRecorder');
recorder.classList.toggle('show');
if (!recorder.classList.contains('show')) {
if (mediaRecorder && mediaRecorder.state === 'recording') {
cancelRecording();
}
}
}
async function toggleRecording() {
const button = document.getElementById('recordButton');
const sendBtn = document.querySelector('#voiceRecorder .save-btn');
const previewAudio = document.getElementById('voicePreview');
const previewContainer = document.getElementById('voicePreviewContainer');
if (!window.MediaRecorder) {
showToast('Voice recording is not supported on this browser', 'error');
return;
}
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
try {
const stream = await getSelectedMicrophone();
const mimeType = getSupportedAudioMimeType();
recordedAudioMimeType = mimeType || 'audio/webm';
mediaRecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
audioChunks = [];
if (previewContainer) previewContainer.style.display = 'none';
if (previewAudio) {
previewAudio.pause();
previewAudio.removeAttribute('src');
previewAudio.load();
}
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: recordedAudioMimeType });
const reader = new FileReader();
reader.onloadend = () => {
window.recordedAudio = reader.result;
showToast('🎧 Recording ready! CLICK PLAY BUTTON to hear it before sending!', 'success');
if (sendBtn) sendBtn.disabled = false;
if (previewContainer) previewContainer.style.display = 'block';
if (previewAudio) {
previewAudio.src = reader.result;
previewAudio.load();
previewAudio.volume = 1.0;
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && previewAudio.setSinkId) {
previewAudio.setSinkId(audioSettings.speakerId).catch(err => {
console.warn('Could not set preview audio output:', err);
});
}
previewAudio.play().catch(err => {
console.log('Autoplay blocked, user must click play:', err);
showToast('⚠️ Click the PLAY button ▶️ below to hear your recording!', 'warning');
});
}
};
reader.readAsDataURL(audioBlob);
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
};
mediaRecorder.start();
button.classList.add('recording');
recordingSeconds = 0;
updateRecordTimer();
recordingInterval = setInterval(updateRecordTimer, 1000);
if (sendBtn) sendBtn.disabled = true;
showToast('Recording started...', 'success');
} catch (error) {
console.error('Error accessing microphone:', error);
showToast('Could not access microphone', 'error');
}
} else {
mediaRecorder.stop();
button.classList.remove('recording');
clearInterval(recordingInterval);
}
}
function updateRecordTimer() {
recordingSeconds++;
const minutes = Math.floor(recordingSeconds / 60);
const seconds = recordingSeconds % 60;
document.getElementById('recordTimer').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
function sendVoiceMessage() {
if (!window.recordedAudio) {
showToast('No recording found', 'warning');
return;
}
if (currentChatContext.type === 'community' || currentChatContext.type === 'group') {
sendRoomMessage({
content: 'Voice message',
mediaType: 'voice',
mediaUrl: window.recordedAudio
});
cancelRecording();
showToast('Voice message posted!');
return;
}
const targetId = ensureChatTarget();
if (!targetId) {
showToast('Unable to find chat target', 'warning');
return;
}
if (!socket || !socket.connected) {
showToast('Connection lost. Cannot send voice message.', 'error');
connectSocket();
return;
}
console.log('🎤 Sending voice message to:', targetId, 'audioSize:', window.recordedAudio?.length);
socket.emit('send dm', {
toUserId: targetId,
content: 'Voice message',
mediaType: 'voice',
mediaUrl: window.recordedAudio
});
cancelRecording();
showToast('Voice message sent!', 'success');
}
function cancelRecording() {
const sendBtn = document.querySelector('#voiceRecorder .save-btn');
const previewAudio = document.getElementById('voicePreview');
const previewContainer = document.getElementById('voicePreviewContainer');
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
document.getElementById('recordButton').classList.remove('recording');
clearInterval(recordingInterval);
document.getElementById('recordTimer').textContent = '00:00';
document.getElementById('voiceRecorder').classList.remove('show');
audioChunks = [];
window.recordedAudio = null;
recordingSeconds = 0;
if (previewContainer) previewContainer.style.display = 'none';
if (previewAudio) {
previewAudio.pause();
previewAudio.removeAttribute('src');
previewAudio.load();
}
if (sendBtn) sendBtn.disabled = true;
}
function handleChatWheel(event) {
const container = document.getElementById('messages-container');
if (!container) return;
event.preventDefault();
const delta = event.deltaY;
container.scrollBy({ top: delta * 0.8, behavior: 'auto' });
}
async function refreshAudioDevices() {
try {
await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(stream => stream.getTracks().forEach(track => track.stop()))
.catch(() => console.log('Microphone permission needed'));
const devices = await navigator.mediaDevices.enumerateDevices();
const micSelect = document.getElementById('microphoneSelector');
const speakerSelect = document.getElementById('speakerSelector');
if (micSelect) {
micSelect.innerHTML = '';
devices.filter(d => d.kind === 'audioinput').forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Microphone ${micSelect.options.length}`;
micSelect.appendChild(option);
});
if (audioSettings.microphoneId && audioSettings.microphoneId !== 'default') {
micSelect.value = audioSettings.microphoneId;
}
}
if (speakerSelect) {
speakerSelect.innerHTML = '';
devices.filter(d => d.kind === 'audiooutput').forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Speaker ${speakerSelect.options.length}`;
speakerSelect.appendChild(option);
});
if (audioSettings.speakerId && audioSettings.speakerId !== 'default') {
speakerSelect.value = audioSettings.speakerId;
}
}
showToast('Audio devices refreshed', 'success');
} catch (error) {
console.error('Error enumerating devices:', error);
showToast('Could not load audio devices', 'warning');
}
}
function saveAudioSettings() {
const micSelect = document.getElementById('microphoneSelector');
const speakerSelect = document.getElementById('speakerSelector');
if (micSelect) audioSettings.microphoneId = micSelect.value;
if (speakerSelect) audioSettings.speakerId = speakerSelect.value;
localStorage.setItem('audioSettings', JSON.stringify(audioSettings));
applySpeakerToAudioElements();
showToast('Audio settings saved', 'success');
}
function applySpeakerToAudioElements() {
const speakerId = audioSettings.speakerId;
if (!speakerId || speakerId === 'default') return;
document.querySelectorAll('audio').forEach(audio => {
if (audio.setSinkId) {
audio.setSinkId(speakerId).catch(err => {
console.warn('Could not set audio output:', err);
});
}
});
const voicePreview = document.getElementById('voicePreview');
if (voicePreview && voicePreview.setSinkId) {
voicePreview.setSinkId(speakerId).catch(err => {
console.warn('Could not set voice preview output:', err);
});
}
}
function loadAudioSettings() {
const saved = localStorage.getItem('audioSettings');
if (saved) {
try {
audioSettings = JSON.parse(saved);
} catch (e) {
console.error('Failed to load audio settings:', e);
}
}
}
async function getSelectedMicrophone() {
const deviceId = audioSettings.microphoneId;
if (!deviceId || deviceId === 'default') {
return navigator.mediaDevices.getUserMedia({ audio: true });
}
return navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: deviceId } }
});
}
function changeTheme(theme) {
themeSettings.mode = theme;
const customSection = document.getElementById('customColorSection');
if (theme === 'custom') {
if (customSection) customSection.style.display = 'block';
applyCustomColors();
} else {
if (customSection) customSection.style.display = 'none';
const root = document.documentElement;
if (theme === 'dark') {
root.style.setProperty('--primary-color', '#5865F2');
root.style.setProperty('--secondary-color', '#4e5058');
root.style.setProperty('--bg-color', '#1e1f22');
root.style.setProperty('--sidebar-color', '#2b2d31');
root.style.setProperty('--text-primary', '#ffffff');
root.style.setProperty('--text-secondary', '#b5bac1');
root.style.setProperty('--border-color', '#3f4147');
} else if (theme === 'light') {
root.style.setProperty('--primary-color', '#5865F2');
root.style.setProperty('--secondary-color', '#d4d7dc');
root.style.setProperty('--bg-color', '#ffffff');
root.style.setProperty('--sidebar-color', '#f2f3f5');
root.style.setProperty('--text-primary', '#060607');
root.style.setProperty('--text-secondary', '#4e5058');
root.style.setProperty('--border-color', '#e3e5e8');
}
}
localStorage.setItem('themeSettings', JSON.stringify(themeSettings));
showToast(`Theme changed to ${theme}`, 'success');
}
function applySimpleCustomColors() {
const root = document.documentElement;
const bg = document.getElementById('bgColorPicker')?.value || '#1e1f22';
const text = document.getElementById('textColorPicker')?.value || '#ffffff';
const icon = document.getElementById('accentColorPicker')?.value || '#5865F2';
root.style.setProperty('--bg-color', bg);
root.style.setProperty('--chat-bg', bg);
root.style.setProperty('--sidebar-color', bg);
root.style.setProperty('--text-primary', text);
root.style.setProperty('--primary-color', icon);
root.style.setProperty('--secondary-color', icon);
root.style.setProperty('--icon-color', icon);
root.style.setProperty('--ui-text-color', '#ffffff');
const simpleTheme = { bg, text, icon, mode: 'custom' };
localStorage.setItem('simpleTheme', JSON.stringify(simpleTheme));
}
function resetSimpleCustomColors() {
document.getElementById('bgColorPicker').value = '#1e1f22';
document.getElementById('textColorPicker').value = '#ffffff';
document.getElementById('accentColorPicker').value = '#5865F2';
const root = document.documentElement;
root.style.setProperty('--bg-color', '#1e1f22');
root.style.setProperty('--chat-bg', '#1e1f22');
root.style.setProperty('--sidebar-color', '#2b2d31');
root.style.setProperty('--text-primary', '#ffffff');
root.style.setProperty('--primary-color', '#5865F2');
root.style.setProperty('--secondary-color', '#5865F2');
root.style.setProperty('--icon-color', '#5865F2');
root.style.setProperty('--ui-text-color', '#ffffff');
localStorage.removeItem('simpleTheme');
showToast('✅ Colors reset to default', 'success');
}
function getBrightness(hexColor) {
const rgb = parseInt(hexColor.replace('#', ''), 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
return (r * 299 + g * 587 + b * 114) / 1000;
}
function getBrightnessAdjusted(hexColor, factor = 0.85) {
const rgb = parseInt(hexColor.replace('#', ''), 16);
let r = (rgb >> 16) & 0xff;
let g = (rgb >> 8) & 0xff;
let b = (rgb >> 0) & 0xff;
r = Math.round(r * factor);
g = Math.round(g * factor);
b = Math.round(b * factor);
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
}
function resetCustomColors() {
themeSettings.customColors = {
primary: '#5865F2',
accent: '#57F287',
background: '#1e1f22',
sidebar: '#2b2d31',
textPrimary: '#ffffff',
textSecondary: '#b5bac1',
iconColor: '#ffffff',
borderColor: '#3f4147'
};
document.getElementById('primaryColorPicker').value = '#5865F2';
document.getElementById('accentColorPicker').value = '#57F287';
document.getElementById('bgColorPicker').value = '#1e1f22';
document.getElementById('sidebarColorPicker').value = '#2b2d31';
document.getElementById('textColorPicker').value = '#ffffff';
document.getElementById('textSecondaryColorPicker').value = '#b5bac1';
document.getElementById('iconColorPicker').value = '#ffffff';
applyCustomColors();
showToast('Colors reset to default', 'success');
}
function loadThemeSettings() {
const saved = localStorage.getItem('themeSettings');
if (saved) {
try {
themeSettings = JSON.parse(saved);
const themeSelector = document.getElementById('themeSelector');
if (themeSelector) themeSelector.value = themeSettings.mode;
if (themeSettings.mode === 'custom') {
const customSection = document.getElementById('customColorSection');
if (customSection) customSection.style.display = 'block';
document.getElementById('primaryColorPicker').value = themeSettings.customColors.primary;
document.getElementById('accentColorPicker').value = themeSettings.customColors.accent;
document.getElementById('bgColorPicker').value = themeSettings.customColors.background;
document.getElementById('sidebarColorPicker').value = themeSettings.customColors.sidebar;
applyCustomColors();
} else {
changeTheme(themeSettings.mode);
}
} catch (e) {
console.error('Failed to load theme settings:', e);
}
}
}
let messageReactions = {};
function showQuickReactions(messageId, messageDiv, messageData = null) {
if (!messageDiv) return;
const quickReactions = ['👍', '❤️', '😂', '😮', '😢', '🙏', '🔥', '🎉', '💯', '👏', '🤯', '😎'];
document.querySelectorAll('.reaction-picker').forEach(p => p.remove());
const picker = document.createElement('div');
picker.className = 'reaction-picker';
picker.style.position = 'fixed';
picker.style.background = 'var(--chat-bg)';
picker.style.border = '2px solid var(--primary-color)';
picker.style.borderRadius = '12px';
picker.style.padding = '12px';
picker.style.display = 'flex';
picker.style.flexDirection = 'column';
picker.style.gap = '10px';
picker.style.zIndex = '9999';
picker.style.boxShadow = '0 8px 24px rgba(0,0,0,0.6)';
picker.style.maxWidth = 'min(96vw, 460px)';
const rect = messageDiv.getBoundingClientRect();
// Position above message, clamped to viewport
const estimatedPickerH = 260;
const spaceAbove = rect.top;
const spaceBelow = window.innerHeight - rect.bottom;
let top;
if (spaceAbove >= estimatedPickerH + 20) {
top = rect.top - estimatedPickerH - 10;
} else if (spaceBelow >= estimatedPickerH + 20) {
top = rect.bottom + 10;
} else {
// Center on screen if neither fits
top = Math.max(10, (window.innerHeight - estimatedPickerH) / 2);
}
picker.style.left = Math.max(12, Math.min(rect.left, window.innerWidth - 470)) + 'px';
picker.style.top = Math.max(12, top) + 'px';
const title = document.createElement('div');
title.textContent = 'React or take action';
title.style.fontSize = '12px';
title.style.fontWeight = '700';
title.style.color = 'var(--text-secondary)';
picker.appendChild(title);
const emojiGrid = document.createElement('div');
emojiGrid.style.display = 'grid';
emojiGrid.style.gridTemplateColumns = 'repeat(6, minmax(0, 1fr))';
emojiGrid.style.gap = '8px';
quickReactions.forEach(emoji => {
const btn = document.createElement('button');
btn.textContent = emoji;
btn.style.border = '1px solid var(--border-color)';
btn.style.background = 'var(--bg-color)';
btn.style.cursor = 'pointer';
btn.style.fontSize = '28px';
btn.style.padding = '8px 12px';
btn.style.borderRadius = '8px';
btn.style.transition = 'all 0.15s';
btn.onmouseover = () => {
btn.style.transform = 'scale(1.25)';
btn.style.background = 'var(--primary-color)';
};
btn.onmouseout = () => {
btn.style.transform = 'scale(1)';
btn.style.background = 'var(--bg-color)';
};
btn.onclick = (e) => {
e.stopPropagation();
addReaction(messageId, emoji);
picker.remove();
};
emojiGrid.appendChild(btn);
});
picker.appendChild(emojiGrid);
const actionsRow = document.createElement('div');
actionsRow.style.display = 'grid';
actionsRow.style.gridTemplateColumns = 'repeat(2, minmax(0, 1fr))';
actionsRow.style.gap = '8px';
const makeActionButton = (label, onClick, isDanger = false) => {
const actionBtn = document.createElement('button');
actionBtn.textContent = label;
actionBtn.style.padding = '8px 10px';
actionBtn.style.borderRadius = '8px';
actionBtn.style.border = `1px solid ${isDanger ? 'var(--danger)' : 'var(--border-color)'}`;
actionBtn.style.background = isDanger ? 'rgba(240,71,71,0.15)' : 'var(--bg-color)';
actionBtn.style.color = isDanger ? 'var(--danger)' : 'var(--text-primary)';
actionBtn.style.fontSize = '12px';
actionBtn.style.fontWeight = '600';
actionBtn.style.cursor = 'pointer';
actionBtn.onclick = (e) => {
e.stopPropagation();
onClick();
picker.remove();
};
return actionBtn;
};
actionsRow.appendChild(makeActionButton('↩ Reply', () => {
const data = messageData || {
fromUsername: messageDiv.querySelector('.message-username')?.textContent || 'user',
content: messageDiv.querySelector('.message-text')?.textContent || ''
};
replyToMessage(messageId, data);
}));
actionsRow.appendChild(makeActionButton('📋 Copy', () => copyMessageText(messageId)));
actionsRow.appendChild(makeActionButton('📦 Archive', () => archiveSingleMessage(messageId, messageDiv)));
if (String(messageDiv.dataset.sender) === String(currentUser?.id)) {
actionsRow.appendChild(makeActionButton('🗑 Delete Message', () => deleteMessage(messageId, messageDiv), true));
}
picker.appendChild(actionsRow);
document.body.appendChild(picker);
const closePickerHandler = (e) => {
if (!picker.contains(e.target)) {
picker.remove();
document.removeEventListener('click', closePickerHandler);
}
};
setTimeout(() => document.addEventListener('click', closePickerHandler), 100);
}
function showReactionPicker(messageId) {
showQuickReactions(messageId, document.querySelector(`[data-message-id="${messageId}"]`));
}
function addReaction(messageId, emoji) {
if (!messageReactions[messageId]) {
messageReactions[messageId] = {};
}
if (!messageReactions[messageId][emoji]) {
messageReactions[messageId][emoji] = [];
}
const userId = currentUser?.id || 'anonymous';
const userIndex = messageReactions[messageId][emoji].indexOf(userId);
if (userIndex > -1) {
messageReactions[messageId][emoji].splice(userIndex, 1);
if (messageReactions[messageId][emoji].length === 0) {
delete messageReactions[messageId][emoji];
}
} else {
messageReactions[messageId][emoji].push(userId);
updateUserStats('reactionsGiven', 1);
}
renderMessageReactions(messageId);
updateStatsDisplay();
showToast(`Reacted with ${emoji}`, 'success');
}
function showMessageContextMenu(e, messageDiv, data) {
document.querySelectorAll('.message-context-menu').forEach(m => m.remove());
const menu = document.createElement('div');
menu.className = 'message-context-menu';
menu.style.position = 'fixed';
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
menu.style.background = 'var(--chat-bg)';
menu.style.border = '2px solid var(--primary-color)';
menu.style.borderRadius = '8px';
menu.style.zIndex = '10000';
menu.style.minWidth = '200px';
menu.style.boxShadow = '0 8px 24px rgba(0,0,0,0.6)';
menu.style.overflow = 'hidden';
const menuItems = [
{ label: '👍 React', action: () => showQuickReactions(messageDiv.dataset.messageId, messageDiv, data), hoverClass: 'primary-hover' },
{ label: '↩️ Reply', action: () => replyToMessage(messageDiv.dataset.messageId, data), hoverClass: 'primary-hover' },
{ label: '📋 Copy', action: () => copyMessageText(messageDiv.dataset.messageId), hoverClass: 'success-hover' },
{ label: '📦 Archive', action: () => archiveSingleMessage(messageDiv.dataset.messageId, messageDiv), hoverClass: 'primary-hover' },
];
if (String(data.from) === String(currentUser?.id)) {
menuItems.push({ label: '✏️ Edit', action: () => editMessage(messageDiv.dataset.messageId, messageDiv.querySelector('.message-text')), hoverClass: 'primary-hover' });
menuItems.push({ label: '🗑️ Delete', action: () => deleteMessage(messageDiv.dataset.messageId, messageDiv), hoverClass: 'danger-hover' });
}
menuItems.forEach(item => {
const btn = document.createElement('div');
btn.textContent = item.label;
btn.style.padding = '12px 16px';
btn.style.cursor = 'pointer';
btn.style.color = 'var(--text-primary)';
btn.style.transition = 'all 0.2s';
btn.style.fontWeight = '500';
btn.style.borderBottom = '1px solid var(--border-color)';
btn.onmouseover = () => {
if (item.hoverClass === 'danger-hover') {
btn.style.background = 'var(--danger)';
btn.style.color = 'white';
btn.style.transform = 'scale(1.02)';
} else if (item.hoverClass === 'success-hover') {
btn.style.background = 'var(--success)';
btn.style.color = 'white';
btn.style.transform = 'scale(1.02)';
} else {
btn.style.background = 'var(--primary-color)';
btn.style.color = 'white';
btn.style.transform = 'scale(1.02)';
}
};
btn.onmouseout = () => {
btn.style.background = 'transparent';
btn.style.color = 'var(--text-primary)';
btn.style.transform = 'scale(1)';
};
btn.onclick = () => {
item.action();
menu.remove();
};
menu.appendChild(btn);
});
const lastBtn = menu.lastElementChild;
if (lastBtn) lastBtn.style.borderBottom = 'none';
document.body.appendChild(menu);
document.addEventListener('click', () => menu.remove(), { once: true });
}
function showFriendContextMenu(e, friendId, friendName) {
document.querySelectorAll('.message-context-menu').forEach(m => m.remove());
const menu = document.createElement('div');
menu.className = 'message-context-menu';
menu.style.position = 'fixed';
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
menu.style.background = 'var(--chat-bg)';
menu.style.border = '2px solid var(--primary-color)';
menu.style.borderRadius = '8px';
menu.style.zIndex = '10000';
menu.style.minWidth = '200px';
menu.style.boxShadow = '0 8px 24px rgba(0,0,0,0.6)';
menu.style.overflow = 'hidden';
const menuItems = [
{ label: '👤 View Profile', action: () => visitUserProfile(friendId, friendName), hoverClass: 'primary-hover' },
{ label: '💬 Send Message', action: () => openDM(friendId, friendName), hoverClass: 'success-hover' },
{ label: '📞 Video Call', action: () => { currentChatContext = { id: friendId, name: friendName, type: 'dm' }; openVideoCallModal(); }, hoverClass: 'primary-hover' },
];
menuItems.forEach(item => {
const btn = document.createElement('div');
btn.textContent = item.label;
btn.style.padding = '12px 16px';
btn.style.cursor = 'pointer';
btn.style.color = 'var(--text-primary)';
btn.style.transition = 'all 0.2s';
btn.style.fontWeight = '500';
btn.style.borderBottom = '1px solid var(--border-color)';
btn.onmouseover = () => {
if (item.hoverClass === 'danger-hover') {
btn.style.background = 'var(--danger)';
btn.style.color = 'white';
btn.style.transform = 'scale(1.02)';
} else if (item.hoverClass === 'success-hover') {
btn.style.background = 'var(--success)';
btn.style.color = 'white';
btn.style.transform = 'scale(1.02)';
} else {
btn.style.background = 'var(--primary-color)';
btn.style.color = 'white';
btn.style.transform = 'scale(1.02)';
}
};
btn.onmouseout = () => {
btn.style.background = 'transparent';
btn.style.color = 'var(--text-primary)';
btn.style.transform = 'scale(1)';
};
btn.onclick = () => {
item.action();
menu.remove();
};
menu.appendChild(btn);
});
const lastBtn = menu.lastElementChild;
if (lastBtn) lastBtn.style.borderBottom = 'none';
document.body.appendChild(menu);
document.addEventListener('click', () => menu.remove(), { once: true });
}
function renderMessageReactions(messageId) {
const messageDiv = document.querySelector(`[data-message-id="${messageId}"]`);
if (!messageDiv) return;
const reactionsDiv = messageDiv.querySelector('.message-reactions');
if (!reactionsDiv) return;
reactionsDiv.innerHTML = '';
const reactions = messageReactions[messageId] || {};
Object.entries(reactions).forEach(([emoji, users]) => {
if (!users || users.length === 0) return;
const reactionItem = document.createElement('div');
reactionItem.className = 'reaction-item';
reactionItem.innerHTML = `${emoji} ${users.length}`;
reactionItem.style.cursor = 'pointer';
reactionItem.onclick = () => addReaction(messageId, emoji);
reactionsDiv.appendChild(reactionItem);
});
}
function replyToMessage(messageId, data) {
const input = document.getElementById('messageInput');
if (input) {
input.value = `> ${data.fromUsername}: ${data.content}\n`;
input.focus();
showToast('Replying to message...', 'success');
}
}
function copyMessageText(messageId) {
const messageDiv = document.querySelector(`[data-message-id="${messageId}"]`);
if (!messageDiv) {
showToast('❌ Message not found', 'error');
return;
}
const textElement = messageDiv.querySelector('.message-text');
const imgElement = messageDiv.querySelector('img');
const vidElement = messageDiv.querySelector('video');
const audioElement = messageDiv.querySelector('audio');
if (textElement && textElement.textContent && textElement.textContent.trim()) {
navigator.clipboard.writeText(textElement.textContent).then(() => {
showToast('✅ Text copied!', 'success');
}).catch(() => {
showToast('❌ Failed to copy', 'error');
});
} else if (imgElement || vidElement || audioElement) {
const url = imgElement?.src || vidElement?.src || audioElement?.src || '';
if (url) {
if (imgElement) {
addToMediaPreview('image', url);
} else if (vidElement) {
addToMediaPreview('video', url);
}
navigator.clipboard.writeText(url).then(() => {
const mediaType = imgElement ? 'Image' : vidElement ? 'Video' : 'Audio';
showToast(`✅ ${mediaType} copied! View in preview panel.`, 'success');
if (copiedMediaItems.length > 0) {
setTimeout(() => openMediaPreview(), 500);
}
}).catch(() => {
showToast('❌ Failed to copy', 'error');
});
} else {
showToast('❌ No media URL found', 'warning');
}
} else {
showToast('❌ No content to copy', 'warning');
}
}
function editMessage(messageId, textElement) {
const currentText = textElement.textContent;
customPrompt('Edit your message:', '✏️ Edit Message', 'Enter new text...', currentText, '✏️').then(newText => {
if (newText && newText.trim() !== '' && newText !== currentText) {
textElement.textContent = newText;
textElement.style.fontStyle = 'italic';
textElement.title = 'Edited';
const editedLabel = document.createElement('span');
editedLabel.textContent = ' (edited)';
editedLabel.style.fontSize = '10px';
editedLabel.style.color = 'var(--text-secondary)';
textElement.appendChild(editedLabel);
showToast('✅ Message edited', 'success');
}
});
}
function deleteMessage(messageId, messageDiv) {
customConfirm(
'This message will be permanently deleted. This action cannot be undone.',
'🗑️ Delete Message',
'🗑️',
true
).then(confirmed => {
if (confirmed) {
messageDiv.style.transition = 'all 0.3s ease-out';
messageDiv.style.opacity = '0';
messageDiv.style.transform = 'translateX(-100%)';
setTimeout(() => {
messageDiv.remove();
showToast('✅ Message deleted', 'success');
}, 300);
}
});
}
function createNewGroup() {
customPrompt('Enter a name for your new group:', '👥 Create Group', 'My Awesome Group', '', '👥').then(groupName => {
if (groupName === null) {
showToast('Group creation cancelled', 'info');
return;
}
if (!groupName || !groupName.trim()) {
showToast('Group name is required', 'warning');
return;
}
const normalizedName = groupName.trim();
customPrompt(
'Invite member IDs (optional, comma-separated):',
'📧 Invite Friends',
'e.g., 123, 456, 789',
'',
'📧'
).then(memberIds => {
const inviteIds = String(memberIds || '')
.split(',')
.map(id => id.trim())
.filter(Boolean);
if (!joinedGroups.includes(normalizedName)) {
joinedGroups.push(normalizedName);
localStorage.setItem('joinedGroups', JSON.stringify(joinedGroups));
}
const profile = getGroupProfile(normalizedName);
if (!profile.icon) {
profile.icon = '👥';
groupProfiles[normalizedName] = profile;
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
}
if (!groupMemberRoles[normalizedName]) {
groupMemberRoles[normalizedName] = {};
}
const ownerId = String(currentUser?.id || 'self');
groupMemberRoles[normalizedName][ownerId] = 'Owner';
inviteIds.forEach((id) => {
const targetId = String(id);
if (targetId && !groupMemberRoles[normalizedName][targetId]) {
groupMemberRoles[normalizedName][targetId] = 'Member';
}
});
localStorage.setItem('groupMemberRoles', JSON.stringify(groupMemberRoles));
loadFriendsForDM();
openGroupChat(normalizedName);
showToast(`✅ Group "${normalizedName}" created${inviteIds.length ? ` with ${inviteIds.length} invited member(s)` : ''}!`, 'success');
});
});
}
function changeGroupPhoto() {
if (currentChatContext.type !== 'group') {
showToast('Open a group chat first', 'warning');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
const groupIndex = joinedGroups.findIndex(g => g.id === currentChatContext.id);
if (groupIndex > -1) {
joinedGroups[groupIndex].avatar = reader.result;
localStorage.setItem('joinedGroups', JSON.stringify(joinedGroups));
showToast('Group photo updated!', 'success');
loadFriendsForDM();
}
};
reader.readAsDataURL(file);
};
input.click();
}
function startVideoCall() {
if (!currentChatContext || currentChatContext.type === 'none' || !currentChatContext.id) {
showToast('❌ Please open a chat first', 'warning');
return;
}
if (currentChatContext.type !== 'dm') {
showToast('Video calls are currently available for direct chats only', 'warning');
return;
}
const targetName = currentChatContext.name || 'User';
const targetId = currentChatContext.id;
const callType = 'Video Call';
openVideoCallModal(targetName, targetId, callType);
}
let archivedChats = JSON.parse(localStorage.getItem('archivedChats') || '[]');
function toggleArchiveChat() {
if (currentChatContext.type === 'none' || !currentChatContext.id) {
showToast('Open a chat first', 'warning');
return;
}
const chatId = currentChatContext.id;
const chatName = currentChatContext.name || 'Unknown';
const chatType = currentChatContext.type;
const archiveIndex = archivedChats.findIndex(c => c.id === chatId && c.type === chatType);
if (archiveIndex > -1) {
archivedChats.splice(archiveIndex, 1);
showToast(`${chatName} unarchived`, 'success');
} else {
archivedChats.push({ id: chatId, name: chatName, type: chatType });
showToast(`${chatName} archived`, 'success');
if (chatType === 'dm') {
const friendIndex = friends.findIndex(f => f.id === chatId);
if (friendIndex > -1) {
friends.splice(friendIndex, 1);
localStorage.setItem('friends', JSON.stringify(friends));
}
}
closeCurrentChat();
}
localStorage.setItem('archivedChats', JSON.stringify(archivedChats));
loadFriendsForDM();
}
function showArchivedChats() {
if (archivedChats.length === 0) {
showToast('No archived chats yet', 'warning');
return;
}
const chatsHtml = archivedChats.map((chat, idx) => {
const icon = chat.type === 'dm' ? '👤' : chat.type === 'community' ? '🌐' : '👥';
return `
${icon}
${chat.name}
${chat.type === 'dm' ? 'Direct Message' : chat.type === 'community' ? 'Community' : 'Group Chat'}
`;
}).join('');
const modal = document.getElementById('archivedChatsModal');
const list = document.getElementById('archivedChatsList');
if (!modal || !list) {
showToast('Archive UI unavailable', 'error');
return;
}
list.innerHTML = chatsHtml;
modal.classList.remove('hidden');
}
function closeArchivedChatsModal() {
document.getElementById('archivedChatsModal')?.classList.add('hidden');
}
function openArchivedChat(type, id, name) {
if (type === 'dm') {
openDM(id, name);
} else if (type === 'community') {
openCommunityChat(name);
} else if (type === 'group') {
openGroupChat(name);
}
}
function unarchiveChat(type, id) {
const index = archivedChats.findIndex(c => c.type === type && c.id === id);
if (index > -1) {
const chat = archivedChats[index];
archivedChats.splice(index, 1);
localStorage.setItem('archivedChats', JSON.stringify(archivedChats));
showToast(`${chat.name} unarchived`, 'success');
loadFriendsForDM();
showArchivedChats();
}
}
function deleteArchivedChat(type, id) {
const index = archivedChats.findIndex(c => c.type === type && c.id === id);
if (index === -1) return;
const chat = archivedChats[index];
archivedChats.splice(index, 1);
localStorage.setItem('archivedChats', JSON.stringify(archivedChats));
showToast(`Deleted archived chat: ${chat.name}`, 'success');
showArchivedChats();
}
function clearArchivedChats() {
archivedChats = [];
localStorage.setItem('archivedChats', JSON.stringify(archivedChats));
showToast('All archived chats deleted', 'success');
closeArchivedChatsModal();
}
let userStats = JSON.parse(localStorage.getItem('userStats') || '{"messagesCount": 0, "totalChars": 0, "reactionsGiven": 0, "archiveCount": 0, "quizCorrect": 0}');
function updateUserStats(type, value = 1) {
if (!userStats[type]) userStats[type] = 0;
userStats[type] += value;
localStorage.setItem('userStats', JSON.stringify(userStats));
}
function showUserStats() {
const stats = userStats;
const statsHtml = `
📊 Your Statistics
Messages Sent
${stats.messagesCount || 0}
Characters Typed
${stats.totalChars || 0}
Reactions Given
${stats.reactionsGiven || 0}
Quiz Correct
${stats.quizCorrect || 0}
`;
const modal = document.createElement('div');
modal.innerHTML = statsHtml;
modal.style.position = 'fixed';
modal.style.top = '50%';
modal.style.left = '50%';
modal.style.transform = 'translate(-50%, -50%)';
modal.style.zIndex = '5000';
const backdrop = document.createElement('div');
backdrop.style.position = 'fixed';
backdrop.style.top = '0';
backdrop.style.left = '0';
backdrop.style.width = '100%';
backdrop.style.height = '100%';
backdrop.style.background = 'rgba(0,0,0,0.6)';
backdrop.style.zIndex = '4999';
backdrop.onclick = () => { backdrop.remove(); modal.remove(); };
document.body.appendChild(backdrop);
document.body.appendChild(modal);
}
function startQuiz() {
const html = `
Complete challenges to earn coins and boost your profile stats.
`;
customAlert(html, '🎉 Events & Challenges', '🎉');
}
function startTriviaChallenge() {
const rounds = [
{ q: 'Which keyboard shortcut is commonly used for copy on Windows?', a: ['ctrl+c', 'control+c'] },
{ q: 'If a message is end-to-end encrypted, who should be able to read it?', a: ['only sender and receiver', 'sender and receiver', 'only the sender and receiver'] },
{ q: 'What HTTP status code usually means “Not Found”?', a: ['404'] },
{ q: 'What does DM stand for in chat apps?', a: ['direct message'] },
{ q: 'Which is safer for account protection: weak password or two-factor authentication?', a: ['two-factor authentication', '2fa'] }
];
const picks = rounds.sort(() => Math.random() - 0.5).slice(0, 3);
let score = 0;
let index = 0;
const askNext = () => {
if (index >= picks.length) {
const reward = 15 + (score * 12);
addCoins(reward);
if (score >= 2) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`🧠 Trivia complete: ${score}/3, +${reward} coins`, score >= 2 ? 'success' : 'warning');
return;
}
const round = picks[index];
customPrompt(`Question ${index + 1}/3\n\n${round.q}`, '🧠 Trivia Challenge', 'Type your answer...', '', '🧠').then((answer) => {
if (answer === null) return;
const guess = String(answer).trim().toLowerCase();
if (round.a.includes(guess)) score++;
index += 1;
askNext();
});
};
askNext();
}
function startSpeedTypingChallenge() {
const phrases = [
'Ship after testing, not before.',
'Fast replies still need clear thinking.',
'Reliable chat depends on stable signals.'
];
const picks = phrases.sort(() => Math.random() - 0.5).slice(0, 2);
let index = 0;
let matches = 0;
const startedAt = Date.now();
const askNext = () => {
if (index >= picks.length) {
const elapsed = (Date.now() - startedAt) / 1000;
const reward = matches === 2 ? (elapsed <= 16 ? 55 : 45) : matches === 1 ? 28 : 12;
addCoins(reward);
if (matches === 2) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`⌨ Speed Typing: ${matches}/2 clean rounds, +${reward} coins`, matches > 0 ? 'success' : 'warning');
return;
}
const phrase = picks[index];
customPrompt(`Round ${index + 1}/2\n\nType this exactly:\n\n${phrase}`, '⌨ Speed Typing', 'Type this exactly', '', '⌨').then((answer) => {
if (answer === null) return;
if (String(answer).trim() === phrase) matches++;
index += 1;
askNext();
});
};
askNext();
}
function startMemoryChallenge() {
const rounds = [4, 5];
const pool = ['🔥', '🧿', '🌊', '⚡', '💎', '🎯', '🌙'];
let score = 0;
let index = 0;
const playRound = () => {
if (index >= rounds.length) {
const reward = 18 + (score * 16);
addCoins(reward);
if (score === rounds.length) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`🧩 Memory run: ${score}/2 rounds, +${reward} coins`, score > 0 ? 'success' : 'warning');
return;
}
const seq = Array.from({ length: rounds[index] }, () => pool[Math.floor(Math.random() * pool.length)]);
const expected = seq.join(' ');
customAlert(`${expected}
Round ${index + 1}/2. Memorize the pattern, then press OK.
`, '🧩 Memory Challenge', '🧩').then(() => {
customPrompt('Enter the sequence exactly', '🧩 Memory Recall', 'Type sequence here', '', '🧩').then((answer) => {
if (answer === null) return;
if (String(answer).trim() === expected) score++;
index += 1;
playRound();
});
});
};
playRound();
}
async function startMathChallenge() {
const ops = ['+', '-', '*'];
let score = 0;
for (let i = 0; i < 3; i++) {
const a = Math.floor(Math.random() * 20) + 5;
const b = Math.floor(Math.random() * 11) + 2;
const op = ops[Math.floor(Math.random() * ops.length)];
const answer = op === '+' ? a + b : op === '-' ? a - b : a * b;
const response = await customPrompt(`Problem ${i + 1}/3\n\n${a} ${op} ${b} = ?`, '🧮 Math Blitz', 'Your answer...', '', '🧮');
if (response === null) {
showToast('Math Blitz cancelled', 'warning');
return;
}
if (Number(String(response).trim()) === answer) score++;
}
const reward = 20 + (score * 10);
addCoins(reward);
if (score >= 2) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`🧮 Math Blitz: ${score}/3 correct, +${reward} coins`, score >= 2 ? 'success' : 'warning');
}
async function startRiddleChallenge() {
const riddles = [
{ q: 'I speak without a mouth and hear without ears. What am I?', a: ['echo'] },
{ q: 'The more you take, the more you leave behind. What am I?', a: ['footsteps', 'steps'] },
{ q: 'What has keys but cannot open locks?', a: ['piano', 'a piano'] },
{ q: 'What has to be broken before you can use it?', a: ['egg', 'an egg'] },
{ q: 'What gets wetter the more it dries?', a: ['towel', 'a towel'] }
];
const picks = riddles.sort(() => Math.random() - 0.5).slice(0, 3);
let score = 0;
for (let i = 0; i < picks.length; i++) {
const response = await customPrompt(`Riddle ${i + 1}/3\n\n${picks[i].q}`, '🕵️ Riddle Room', 'Type your answer...', '', '🕵️');
if (response === null) {
showToast('Riddle challenge cancelled', 'warning');
return;
}
const guess = String(response).trim().toLowerCase();
if (picks[i].a.includes(guess)) score++;
}
const reward = 15 + (score * 12);
addCoins(reward);
if (score >= 2) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`🕵️ Riddle Room: ${score}/3 solved, +${reward} coins`, score >= 2 ? 'success' : 'warning');
}
function startReactionChallenge() {
const backdrop = document.createElement('div');
backdrop.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.72);z-index:7000;display:flex;align-items:center;justify-content:center;padding:16px;';
const panel = document.createElement('div');
panel.style.cssText = 'width:min(460px,94vw);background:var(--chat-bg);border:1px solid var(--border-color);border-radius:14px;padding:18px;display:grid;gap:12px;';
panel.innerHTML = `
⚡ Reaction Speed
Press Start, then tap as soon as the card turns green.
`;
backdrop.appendChild(panel);
document.body.appendChild(backdrop);
const targetBtn = panel.querySelector('#reactionTargetBtn');
const startBtn = panel.querySelector('#reactionStartBtn');
const closeBtn = panel.querySelector('#reactionCloseBtn');
let active = false;
let startedAt = 0;
let timer = null;
const cleanup = () => {
if (timer) clearTimeout(timer);
backdrop.remove();
};
closeBtn.onclick = cleanup;
backdrop.onclick = (e) => { if (e.target === backdrop) cleanup(); };
startBtn.onclick = () => {
startBtn.disabled = true;
targetBtn.textContent = 'Get ready...';
targetBtn.style.background = 'var(--bg-color)';
timer = setTimeout(() => {
active = true;
startedAt = performance.now();
targetBtn.textContent = 'TAP NOW!';
targetBtn.style.background = 'var(--success)';
targetBtn.style.color = '#fff';
}, 1400 + Math.random() * 2800);
};
targetBtn.onclick = () => {
if (!startBtn.disabled) return;
if (!active) {
showToast('Too early! Tap after green.', 'warning');
startBtn.disabled = false;
targetBtn.textContent = 'Wait for green...';
if (timer) clearTimeout(timer);
return;
}
const ms = Math.max(1, Math.round(performance.now() - startedAt));
const reward = ms <= 220 ? 85 : ms <= 300 ? 70 : ms <= 380 ? 55 : ms <= 500 ? 38 : 24;
addCoins(reward);
if (ms <= 380) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`⚡ ${ms}ms reaction! +${reward} coins`, 'success');
cleanup();
};
}
async function startWordScrambleChallenge() {
const rounds = [
{ word: 'server', scrambled: 'vreser' },
{ word: 'archive', scrambled: 'vicher a'.replace(' ', '') },
{ word: 'message', scrambled: 'sgemase' },
{ word: 'profile', scrambled: 'elofipr' }
];
let score = 0;
for (let i = 0; i < rounds.length; i++) {
const r = rounds[i];
const response = await customPrompt(`Round ${i + 1}/4\n\nUnscramble: ${r.scrambled}`, '🔤 Word Scramble', 'Type the word...', '', '🔤');
if (response === null) {
showToast('Word Scramble cancelled', 'warning');
return;
}
if (String(response).trim().toLowerCase() === r.word) score++;
}
const reward = 18 + (score * 8);
addCoins(reward);
if (score >= 3) updateUserStats('quizCorrect', 1);
updateStatsDisplay();
updateUserProfile();
showToast(`🔤 Word Scramble: ${score}/4 correct, +${reward} coins`, score >= 2 ? 'success' : 'warning');
}
async function addCoins(amount) {
const userJSON = localStorage.getItem('user');
if (!userJSON) return;
const user = JSON.parse(userJSON);
user.coins = (user.coins || 0) + amount;
localStorage.setItem('user', JSON.stringify(user));
currentUser.coins = user.coins;
// Update displays immediately
const coinsDisplay = document.getElementById('coinsDisplay');
if (coinsDisplay) coinsDisplay.textContent = `💰 Coins: ${user.coins}`;
// Sync to server immediately so shop shows correct balance
await syncCoinsToServer(user.coins);
// Refresh shop if open to show new coin balance
if (!document.getElementById('shopModal')?.classList.contains('hidden')) {
loadShopItems();
}
}
async function syncCoinsToServer(coins) {
if (!currentToken) return;
const userId = resolveCurrentUserId();
if (userId && !currentUser?.id) {
currentUser = { ...(currentUser || {}), id: userId };
}
try {
const response = await fetch(`${API_BASE}/profile/coins`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ coins })
});
if (!response.ok) {
console.warn('Failed to sync coins to server');
}
} catch (err) {
console.warn('Coin sync error:', err);
}
}
function updateUserProfile() {
if (!currentUser) return;
const userJSON = localStorage.getItem('user');
if (userJSON) {
const user = JSON.parse(userJSON);
currentUser = user;
const coinsDisplay = document.getElementById('coinsDisplay');
if (coinsDisplay) coinsDisplay.textContent = `💰 Coins: ${user.coins || 0}`;
if (!document.getElementById('shopModal')?.classList.contains('hidden')) {
loadShopItems();
}
}
}
function updateStatsDisplay() {
const stats = JSON.parse(localStorage.getItem('userStats') || '{"messagesCount": 0, "totalChars": 0, "reactionsGiven": 0, "archiveCount": 0, "quizCorrect": 0}');
userStats = stats;
const messagesCountEl = document.getElementById('statMessagesCount');
const totalCharsEl = document.getElementById('statTotalChars');
const reactionsGivenEl = document.getElementById('statReactionsGiven');
const quizCorrectEl = document.getElementById('statQuizCorrect');
if (messagesCountEl) messagesCountEl.textContent = stats.messagesCount || 0;
if (totalCharsEl) totalCharsEl.textContent = stats.totalChars || 0;
if (reactionsGivenEl) reactionsGivenEl.textContent = stats.reactionsGiven || 0;
if (quizCorrectEl) quizCorrectEl.textContent = stats.quizCorrect || 0;
}
function resetUserStats() {
customConfirm(
'This will reset all your statistics including messages sent, reactions given, and quiz scores. This action cannot be undone.',
'⚠️ Reset Statistics',
'⚠️',
true
).then(confirmed => {
if (confirmed) {
localStorage.setItem('userStats', JSON.stringify({messagesCount: 0, totalChars: 0, reactionsGiven: 0, archiveCount: 0, quizCorrect: 0}));
updateStatsDisplay();
showToast('Statistics reset successfully', 'success');
}
});
}
function showMobileSection(section) {
if (window.innerWidth <= 768) {
closeMobileSidebar();
closeSettings();
closeProfile();
closeCommunitiesModal();
closeGroupsModal();
}
document.querySelectorAll('.mobile-nav-item').forEach(item => {
if (item.getAttribute('data-section') === section) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
switch(section) {
case 'chats':
toggleChannelSidebar();
break;
case 'communities':
openCommunitiesModal();
break;
case 'groups':
openGroupsModal();
break;
case 'friends':
showSection('friends');
break;
case 'settings':
openSettings();
break;
}
}
function closeMobileSidebar() {
document.getElementById('channelSidebar')?.classList.remove('show');
document.getElementById('mobileMembersPanel')?.classList.remove('show');
document.getElementById('sidebarOverlay')?.classList.remove('show');
}
function openQuickActionsMenu() {
const extraActions = [];
if (currentChatContext?.type && currentChatContext.type !== 'none') {
extraActions.push('');
if (currentChatContext.type === 'group') {
extraActions.push('');
extraActions.push('');
}
if (currentChatContext.type === 'community') {
extraActions.push('');
}
}
const html = `
${extraActions.join('')}
`;
customAlert(html, 'Quick Actions', '🧿');
}
function openFireTechBot() {
openDM(FIRETECH_BOT_USER.userId, FIRETECH_BOT_USER.username);
}
function showQuickActionsMenu() {
openQuickActionsMenu();
}
function openArchivedChatsFromQuickActions() {
closeCustomDialog();
setTimeout(() => showArchivedChats(), 40);
}
function toggleMobileMembers() {
const panel = document.getElementById('mobileMembersPanel');
const overlay = document.getElementById('sidebarOverlay');
if (!panel) return;
const isShowing = panel.classList.contains('show');
if (isShowing) {
panel.classList.remove('show');
if (overlay) overlay.classList.remove('show');
} else {
panel.classList.add('show');
if (overlay) overlay.classList.add('show');
updateMobileMembersList();
}
}
function updateMobileMembersList() {
const mobileList = document.getElementById('mobileMembersList');
const desktopList = document.getElementById('membersList');
if (!mobileList || !desktopList) return;
mobileList.innerHTML = desktopList.innerHTML;
}
function getCommunityProfile(name) {
const cacheKey = getRoomCacheKey('community', name);
const synced = roomProfileCache[cacheKey] || {};
if (!communityProfiles[name]) {
communityProfiles[name] = {
name: synced.name || name,
icon: synced.icon || '',
image: synced.image || '',
members: [
{
id: currentUser?.id || `u-${Date.now()}`,
username: currentUser?.username || 'You',
role: 'leader'
}
]
};
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
} else {
const local = communityProfiles[name] || {};
communityProfiles[name] = {
...local,
name: local.name || synced.name || name,
icon: local.icon || synced.icon || '',
image: typeof local.image === 'string' ? local.image : (synced.image || ''),
members: Array.isArray(local.members) ? local.members : []
};
}
return communityProfiles[name];
}
function getGroupProfile(name) {
const cacheKey = getRoomCacheKey('group', name);
const synced = roomProfileCache[cacheKey] || {};
if (!groupProfiles[name]) {
groupProfiles[name] = {
name: synced.name || name,
icon: synced.icon || '👥',
image: synced.image || ''
};
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
} else {
const local = groupProfiles[name] || {};
groupProfiles[name] = {
...local,
name: local.name || synced.name || name,
icon: local.icon || synced.icon || '👥',
image: typeof local.image === 'string' ? local.image : (synced.image || '')
};
}
return groupProfiles[name];
}
function openCommunityDetails(name) {
const profile = getCommunityProfile(name);
const modal = document.getElementById('communityDetailsModal');
const title = document.getElementById('communityDetailsTitle');
const body = document.getElementById('communityDetailsBody');
if (!modal || !title || !body) return;
const roomRole = getMyRoomRole('community', name);
const localRole = Array.isArray(profile.members)
? (profile.members.find((m) => String(m.id) === String(currentUser?.id))?.role || 'member')
: 'member';
const canManage = roomRole === 'Owner' || roomRole === 'Admin' || localRole === 'leader' || localRole === 'vice_leader';
const roleLabel = roomRole === 'Admin' ? 'Vice Leader' : (roomRole === 'Owner' ? 'Leader' : (localRole === 'leader' ? 'Leader' : (localRole === 'vice_leader' ? 'Vice Leader' : 'Member')));
title.textContent = `${profile.name}`;
const members = Array.isArray(profile.members) ? profile.members : [];
const avatarPreview = profile.image
? ``
: `${profile.icon || '🌐'}
`;
body.innerHTML = `
${avatarPreview}
${profile.name}
Community profile preview
Your role: ${roleLabel}. Only Leader or Vice Leader can change community appearance.
Members (${members.length})
${members.map((m) => `
`).join('')}
`;
modal.classList.add('show');
}
function closeCommunityDetails() {
document.getElementById('communityDetailsModal')?.classList.remove('show');
}
function renameCommunityPrompt(name) {
if (!canManageRoomAppearance('community', name)) {
showToast('Only Leader or Vice Leader can rename this community', 'warning');
return;
}
customPrompt('Enter new community name', 'Rename Community', name, '', '✏️').then((newName) => {
if (!newName || !newName.trim() || newName.trim() === name) return;
const trimmed = newName.trim();
const idx = joinedCommunities.indexOf(name);
if (idx >= 0) {
joinedCommunities[idx] = trimmed;
localStorage.setItem('joinedCommunities', JSON.stringify(joinedCommunities));
}
if (communityProfiles[name]) {
communityProfiles[trimmed] = { ...communityProfiles[name], name: trimmed };
delete communityProfiles[name];
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
}
if (currentChatContext.type === 'community' && currentChatContext.name === name) {
openCommunityChat(trimmed);
}
emitRoomProfileUpdate('community', name, { name: trimmed });
loadFriendsForDM();
closeCommunityDetails();
showToast('Community renamed', 'success');
});
}
function changeCommunityIconPrompt(name) {
const commonIcons = ['🌐', '💎', '🎮', '🎨', '🎵', '📚', '⚽', '🍕', '🚀', '💻', '🎯', '🏆', '🎬', '🌟', '❤️', '🔥'];
const iconsHtml = commonIcons.map(icon =>
``
).join('');
const html = `
Choose an icon for your community: "${name}"
${iconsHtml}
`;
customAlert(html, 'Change Community Icon', '🖼️');
}
function setCommunityIcon(name, icon) {
if (!canManageRoomAppearance('community', name)) {
showToast('Only Leader or Vice Leader can change icon', 'warning');
return;
}
const profile = getCommunityProfile(name);
profile.icon = icon;
profile.image = '';
communityProfiles[name] = profile;
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
cacheRoomProfile('community', name, { icon, image: '' });
closeCustomDialog();
emitRoomProfileUpdate('community', name, { icon, image: '' });
refreshRoomVisuals('community', name, { reopenDetails: true });
showToast('Community icon updated! 🎉', 'success');
}
function setCommunityIconFromInput(name) {
const input = document.getElementById('customIconInput');
if (!input || !input.value.trim()) {
showToast('Please enter an emoji', 'warning');
return;
}
setCommunityIcon(name, input.value.trim().slice(0, 2));
}
function changeCommunityImagePrompt(name) {
if (!canManageRoomAppearance('community', name)) {
showToast('Only Leader or Vice Leader can change image', 'warning');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (event) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
const profile = getCommunityProfile(name);
profile.image = reader.result;
communityProfiles[name] = profile;
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
cacheRoomProfile('community', name, { image: reader.result });
if (currentChatContext?.type === 'community' && currentChatContext?.name === name) {
const profileAvatar = document.getElementById('profileAvatar');
if (profileAvatar) {
profileAvatar.style.backgroundImage = `url(${reader.result})`;
profileAvatar.style.backgroundSize = 'cover';
profileAvatar.style.backgroundPosition = 'center';
profileAvatar.textContent = '';
}
const profileName = document.getElementById('profileName');
if (profileName) {
profileName.textContent = name;
}
setTimeout(() => {
const avatarDiv = document.getElementById('profileAvatar');
if (avatarDiv && !avatarDiv.style.backgroundImage) {
avatarDiv.style.backgroundImage = `url(${reader.result})`;
avatarDiv.style.backgroundSize = 'cover';
}
}, 100);
}
refreshRoomVisuals('community', name, { reopenDetails: true });
emitRoomProfileUpdate('community', name, { image: reader.result });
showToast('Community image updated (Profile updated)', 'success');
};
reader.readAsDataURL(file);
};
input.click();
}
function changeGroupImagePrompt(name) {
if (!canManageRoomAppearance('group', name)) {
showToast('Only Owner or Admin can change group image', 'warning');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (event) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
const profile = getGroupProfile(name);
profile.image = reader.result;
groupProfiles[name] = profile;
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
cacheRoomProfile('group', name, { image: reader.result });
if (currentChatContext?.type === 'group' && currentChatContext?.name === name) {
const profileAvatar = document.getElementById('profileAvatar');
if (profileAvatar) {
profileAvatar.style.backgroundImage = `url(${reader.result})`;
profileAvatar.style.backgroundSize = 'cover';
profileAvatar.style.backgroundPosition = 'center';
profileAvatar.textContent = '';
}
const profileName = document.getElementById('profileName');
if (profileName) {
profileName.textContent = name;
}
setTimeout(() => {
const avatarDiv = document.getElementById('profileAvatar');
if (avatarDiv && !avatarDiv.style.backgroundImage) {
avatarDiv.style.backgroundImage = `url(${reader.result})`;
avatarDiv.style.backgroundSize = 'cover';
}
}, 100);
}
refreshRoomVisuals('group', name, { reopenDetails: true });
emitRoomProfileUpdate('group', name, { image: reader.result });
showToast('Group image updated (Profile updated)', 'success');
};
reader.readAsDataURL(file);
};
input.click();
}
function toggleChannelSidebarEnhanced() {
const sidebar = document.getElementById('channelSidebar');
const overlay = document.getElementById('sidebarOverlay');
sidebar?.classList.toggle('show');
overlay?.classList.toggle('show');
}
function togglePinCurrentChat() {
if (!currentChatContext || currentChatContext.type === 'none') {
showToast('No chat selected', 'warning');
return;
}
const chatId = `${currentChatContext.type}:${currentChatContext.id || currentChatContext.name}`;
const index = pinnedChats.indexOf(chatId);
if (index > -1) {
pinnedChats.splice(index, 1);
showToast('Chat unpinned', 'success');
} else {
pinnedChats.push(chatId);
showToast('Chat pinned to top', 'success');
}
localStorage.setItem('pinnedChats', JSON.stringify(pinnedChats));
updatePinButtonState();
loadFriendsForDM();
}
function updatePinButtonState() {
const pinBtn = document.getElementById('pinChatBtn');
if (!pinBtn || !currentChatContext || currentChatContext.type === 'none') return;
const chatId = `${currentChatContext.type}:${currentChatContext.id || currentChatContext.name}`;
const isPinned = pinnedChats.includes(chatId);
pinBtn.innerHTML = isPinned ? '' : '';
pinBtn.title = isPinned ? 'Unpin chat' : 'Pin chat';
}
function isChatPinned(chatType, chatId) {
const chatKey = `${chatType}:${chatId}`;
return pinnedChats.includes(chatKey);
}
function showToastClickable(message, type = 'success', chatContext = null) {
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type} ${chatContext ? 'clickable' : ''}`;
const iconMap = {
success: '✅',
error: '❌',
warning: '⚠️',
info: 'ℹ️'
};
toast.innerHTML = `
${iconMap[type] || iconMap.info}
${type.charAt(0).toUpperCase() + type.slice(1)}
${message}
${chatContext ? '
Click to open
' : ''}
`;
if (chatContext) {
toast.style.cursor = 'pointer';
toast.addEventListener('click', () => {
if (chatContext.type === 'dm') {
openDM(chatContext.friendId, chatContext.friendName);
} else if (chatContext.type === 'community') {
openRoomChat('community', chatContext.name);
} else if (chatContext.type === 'group') {
openRoomChat('group', chatContext.name);
}
toast.remove();
if (window.innerWidth <= 480) {
closeMobileSidebar();
}
});
}
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 5000);
}
function openRoomChat(roomType, roomName) {
if (roomType === 'community') {
openCommunityChat(roomName);
return;
}
if (roomType === 'group') {
openGroupChat(roomName);
}
}
// ====== FIRE APP LOADER ======
function runAppLoader() {
const loader = document.getElementById('appLoader');
const bar = document.getElementById('loaderBar');
const status = document.getElementById('loaderStatus');
if (!loader || !bar) return;
const messages = ['Igniting...', 'Loading modules...', 'Connecting...', 'Almost ready...', ' Ready!'];
let p = 0, msgIdx = 0;
const tick = () => {
p = Math.min(100, p + Math.random() * 22 + 5);
bar.style.width = p + '%';
if (status) status.textContent = messages[Math.min(msgIdx, messages.length - 1)];
msgIdx++;
if (p < 100) {
setTimeout(tick, 80 + Math.random() * 110);
} else {
bar.style.width = '100%';
if (status) status.textContent = ' Ready!';
setTimeout(() => {
loader.classList.add('fade-out');
setTimeout(() => { loader.style.display = 'none'; }, 720);
}, 350);
}
};
setTimeout(tick, 180);
}
// ====== END LOADER ======
window.addEventListener('DOMContentLoaded', () => {
runAppLoader();
loadTheme();
applySettingsToUI();
bindCallControlButtons();
pinnedChats = normalizePinnedChats(JSON.parse(localStorage.getItem('pinnedChats') || '[]'));
localStorage.setItem('pinnedChats', JSON.stringify(pinnedChats));
joinedCommunities = JSON.parse(localStorage.getItem('joinedCommunities') || '[]');
joinedGroups = JSON.parse(localStorage.getItem('joinedGroups') || '[]');
const savedLang = preferredLanguage;
syncAuthLanguage();
localizeStaticUi(currentLanguage);
applyEnhancedEmoji(document.body);
setPickerScale(pickerScale);
ensureLocalizationObserver();
if (savedLang && savedLang !== 'en') setTimeout(() => applyLanguage(savedLang, true), 100);
document.getElementById('desktopNotifications')?.addEventListener('change', saveNotificationSettings);
document.getElementById('messageSound')?.addEventListener('change', saveNotificationSettings);
document.getElementById('enterToSend')?.addEventListener('change', saveChatSettings);
document.getElementById('showTypingIndicator')?.addEventListener('change', saveChatSettings);
document.getElementById('autoplayGifs')?.addEventListener('change', saveChatSettings);
document.getElementById('compactMode')?.addEventListener('change', saveAppearanceSettings);
let typingTimeout = null;
const typingInput = document.getElementById('messageInput');
if (typingInput) {
typingInput.addEventListener('input', () => {
if (!chatSettings.showTypingIndicator) return;
if (currentChatContext.type === 'community' && currentChatContext.name) {
socket?.emit('community typing', { community: currentChatContext.name, username: currentUser?.username });
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket?.emit('community stop typing', { community: currentChatContext.name, username: currentUser?.username });
}, 2000);
} else if (currentChatContext.type === 'group' && currentChatContext.id) {
socket?.emit('room typing', { roomType: 'group', roomId: currentChatContext.id, roomName: currentChatContext.name, username: currentUser?.username });
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket?.emit('room stop typing', { roomType: 'group', roomId: currentChatContext.id, roomName: currentChatContext.name, username: currentUser?.username });
}, 2000);
}
});
}
const messagesContainer = document.getElementById('messages-container');
const scrollRail = document.getElementById('chatScrollRail');
const scrollThumb = document.getElementById('chatScrollThumb');
messagesContainer?.addEventListener('scroll', syncChatScrollbarPosition);
messagesContainer?.addEventListener('wheel', handleChatWheel, { passive: false });
scrollRail?.addEventListener('mousedown', jumpChatScrollbar);
scrollThumb?.addEventListener('mousedown', startChatScrollbarDrag);
document.addEventListener('mousemove', handleChatScrollbarDrag);
document.addEventListener('mouseup', stopChatScrollbarDrag);
window.addEventListener('resize', refreshChatScrollbar);
setTimeout(refreshChatScrollbar, 120);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
document.getElementById('pickerPanel')?.classList.remove('show');
}
});
document.addEventListener('click', (event) => {
const picker = document.getElementById('pickerPanel');
if (!picker || !picker.classList.contains('show')) return;
if (picker.contains(event.target)) return;
if (event.target.closest('[onclick="togglePicker()"]')) return;
picker.classList.remove('show');
});
loadAudioSettings();
loadThemeSettings();
refreshAudioDevices();
updateStatsDisplay();
applyOrbEffect(selectedOrbEffect, { silent: true });
renderSavedItemsSummary();
loadSavedBanner();
// Load previous notifications count
notifUnreadCount = notifList.filter(n => !n.read).length;
updateNotifBadge();
// Start debug auto-refresh if call is active
setInterval(() => {
if (peerConnection) refreshCallDebug();
}, 3000);
syncBuildVersionBadge();
ensureRtcConfigLoaded();
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
if (token && user) {
currentToken = token;
currentUser = JSON.parse(user);
showMainApp();
connectSocket();
} else {
document.getElementById('authContainer').style.display = 'flex';
document.getElementById('mainApp').classList.add('hidden');
}
});
function customAlert(message, title = 'Notice', icon = 'ℹ️') {
return new Promise((resolve) => {
const dialog = document.getElementById('customDialog');
dialog.innerHTML = `
`;
dialog.style.display = 'block';
window.customDialogResolve = resolve;
});
}
function customConfirm(message, title = 'Confirm', icon = '❓', isDanger = false) {
return new Promise((resolve) => {
const dialog = document.getElementById('customDialog');
const btnClass = isDanger ? 'custom-dialog-btn-danger danger-hover' : 'custom-dialog-btn-primary primary-hover';
const confirmText = isDanger ? 'Delete' : 'Confirm';
dialog.innerHTML = `
${message}
`;
dialog.style.display = 'block';
window.customDialogResolve = resolve;
});
}
function customPrompt(message, title = 'Input', placeholder = '', defaultValue = '', icon = '✏️') {
return new Promise((resolve) => {
const dialog = document.getElementById('customDialog');
dialog.innerHTML = `
`;
dialog.style.display = 'block';
setTimeout(() => document.getElementById('customDialogInput')?.focus(), 100);
window.customDialogResolve = resolve;
});
}
function closeCustomDialog() {
stopIncomingCallAlert();
document.getElementById('customDialog').style.display = 'none';
if (window.customDialogResolve) window.customDialogResolve(false);
}
function resolveCustomDialog(value) {
stopIncomingCallAlert();
document.getElementById('customDialog').style.display = 'none';
if (window.customDialogResolve) window.customDialogResolve(value);
}
function showQRCodeModal() {
const modal = document.getElementById('qrModal');
const qrContainer = document.querySelector('.qr-code-container');
const userIdSpan = document.getElementById('qrUserId');
if (!currentUser || !currentUser.id) {
showToast('❌ User not logged in', 'error');
return;
}
qrContainer.innerHTML = '';
const qrDiv = document.createElement('div');
qrDiv.id = 'qrCodeCanvas';
qrDiv.style.backgroundColor = '#ffffff';
qrDiv.style.padding = '10px';
qrDiv.style.borderRadius = '8px';
qrDiv.style.display = 'inline-block';
qrContainer.appendChild(qrDiv);
userIdSpan.textContent = currentUser.id || currentUser._id || 'Unknown';
const profileLinkData = `${window.location.origin}?addFriend=${encodeURIComponent(currentUser.id || currentUser._id)}`;
if (typeof QRCode !== 'undefined') {
try {
new QRCode(qrDiv, {
text: profileLinkData,
width: 200,
height: 200,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H
});
console.log('✅ QR Code generated successfully');
} catch (error) {
console.error('❌ QRCode library error:', error);
generateQRCodeFallback(qrDiv, profileLinkData);
}
} else {
console.warn('⚠️ QRCode library not loaded, using fallback');
generateQRCodeFallback(qrDiv, profileLinkData);
}
modal.classList.add('show');
}
function generateQRCodeFallback(container, data) {
console.log('📱 Using QR Code API fallback');
const img = new Image();
img.style.width = '200px';
img.style.height = '200px';
img.style.display = 'block';
img.style.backgroundColor = '#ffffff';
img.style.borderRadius = '8px';
img.onload = () => {
console.log('✅ QR Code loaded from API');
container.innerHTML = '';
container.appendChild(img);
};
img.onerror = () => {
console.error('❌ QR Code API failed');
container.innerHTML = `
📱
Friend Request
${currentUser.username || 'User'}
ID: ${(currentUser.id || currentUser._id || 'N/A').substring(0, 8)}...
`;
};
img.src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data)}`;
}
function closeQRCodeModal() {
document.getElementById('qrModal').classList.remove('show');
}
function scanQRCodeForFriend() {
customPrompt(
'Enter the QR code data or User ID to send a friend request:',
'📷 Scan QR Code',
'FRIEND_REQUEST:123:Username or just 123',
'',
'📷'
).then(qrData => {
if (!qrData) return;
let friendId = null;
let friendName = 'User';
if (qrData.startsWith('http://') || qrData.startsWith('https://')) {
try {
const parsed = new URL(qrData.trim());
const addFriendId = parsed.searchParams.get('addFriend');
if (addFriendId) {
friendId = addFriendId;
friendName = 'Friend';
}
} catch (error) {
console.warn('Invalid QR URL:', error);
}
} else if (qrData.startsWith('FRIEND_REQUEST:')) {
const parts = qrData.split(':');
friendId = parts[1];
friendName = parts[2] || 'User';
} else {
friendId = qrData.trim();
}
if (!friendId) {
showToast('❌ Invalid QR code data', 'error');
return;
}
customConfirm(
`Send friend request to ${friendName} (ID: ${friendId})?`,
'👤 Friend Request',
'👤',
false
).then(confirmed => {
if (confirmed) {
if (socket && socket.connected) {
socket.emit('send friend request', { toUserId: friendId });
showToast(`✅ Friend request sent to ${friendName}!`, 'success');
} else {
showToast('❌ Not connected to server', 'error');
}
}
});
});
}
function openVideoCallModal(targetName, targetId, callType) {
targetName = targetName || currentChatContext.name || 'User';
targetId = targetId || currentChatContext.id;
callType = callType || 'Video Call';
openCallDialog(targetName);
setCallStatus('Dialing...');
appendCallLog(`Outgoing ${callType.toLowerCase()} to ${targetName}`);
if (socket && socket.connected && targetId) {
socket.emit('start video call', {
targetId: targetId,
type: currentChatContext.type || 'dm',
targetName: targetName
});
activeCallState = { callId: null, peerId: targetId, direction: 'outgoing' };
showToast(`📞 Calling ${targetName}...`, 'success');
appendCallLog('Call request emitted to signaling server');
}
}
function handleIncomingVideoCall(payload) {
if (!payload || !payload.callId) return;
const callerName = payload.fromUsername || 'Unknown User';
activeCallState = { callId: payload.callId, peerId: payload.fromId, direction: 'incoming' };
startIncomingCallAlert();
customConfirm(
`${callerName} is calling you. Accept this video call?`,
'Incoming Video Call',
'📞',
false
).then((accepted) => {
if (!socket || !socket.connected || !activeCallState) {
stopIncomingCallAlert();
return;
}
if (accepted) {
stopIncomingCallAlert();
appendCallLog(`Accepted incoming call from ${callerName}`);
socket.emit('accept video call', {
callId: activeCallState.callId,
toId: activeCallState.peerId
});
closeCustomDialog();
startHostedCallRoom(callerName, `firetech-${activeCallState.callId}`).catch((error) => {
console.error('Failed to initialize hosted call session:', error);
showToast('Unable to join hosted call', 'error');
cleanupCallSession(false);
});
} else {
stopIncomingCallAlert();
socket.emit('reject video call', {
callId: activeCallState.callId,
toId: activeCallState.peerId
});
showToast(`Missed call from ${callerName}`, 'warning');
activeCallState = null;
}
});
}
async function ensureLocalCallStream() {
if (localCallStream) return localCallStream;
if (!window.isSecureContext && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
const secureContextError = new Error('Camera and microphone require HTTPS on non-localhost devices.');
showToast('❌ Call media blocked: open app over HTTPS on both devices.', 'error');
throw secureContextError;
}
try {
const selectedMic = audioSettings?.microphoneId;
const constraints = {
audio: true, // Keep it simple for better compatibility
video: {
facingMode: 'user',
width: { min: 320, ideal: 640, max: 1280 }, // Use ranges, not fixed ideals
height: { min: 240, ideal: 480, max: 720 }
}
};
localCallStream = await navigator.mediaDevices.getUserMedia(constraints);
showToast('📹 Camera and microphone enabled', 'success');
return localCallStream;
} catch (error) {
console.error('Media access error:', error);
// Retry with simpler constraints for mobile browsers that reject advanced constraints.
try {
localCallStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
showToast('📹 Camera and microphone enabled', 'success');
return localCallStream;
} catch (secondaryError) {
console.warn('Secondary media constraint attempt failed:', secondaryError);
}
try {
localCallStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
showToast('🎤 Microphone enabled (camera unavailable on this device)', 'warning');
return localCallStream;
} catch (fallbackError) {
showToast(`❌ Cannot access camera/mic: ${fallbackError.message}`, 'error');
throw fallbackError;
}
}
}
function createPeerConnection(peerId) {
if (peerConnection) {
peerConnection.onicecandidate = null;
peerConnection.ontrack = null;
peerConnection.onconnectionstatechange = null;
peerConnection.oniceconnectionstatechange = null;
peerConnection.close();
peerConnection = null;
}
const runtimeIceServers = Array.isArray(rtcRuntimeConfig?.iceServers) && rtcRuntimeConfig.iceServers.length
? rtcRuntimeConfig.iceServers
: getDefaultIceServers();
const hasTurnServersConfigured = hasTurnServerConfigured(runtimeIceServers);
activeIceServers = runtimeIceServers;
peerConnection = new RTCPeerConnection({
iceServers: runtimeIceServers,
iceCandidatePoolSize: 10,
iceTransportPolicy: forceRelayOnly ? 'relay' : (rtcRuntimeConfig?.iceTransportPolicy || 'all')
});
remoteIceCandidatesQueue = [];
remoteCallStream = new MediaStream();
const remoteVideo = document.getElementById('remoteCallVideo');
if (remoteVideo) {
remoteVideo.srcObject = remoteCallStream;
remoteVideo.autoplay = true;
remoteVideo.playsInline = true;
remoteVideo.muted = false;
remoteVideo.volume = 1.0;
}
let hasLocalAudio = false;
let hasLocalVideo = false;
if (localCallStream) {
localCallStream.getTracks().forEach((track) => {
track.enabled = true;
peerConnection.addTrack(track, localCallStream);
if (track.kind === 'audio') hasLocalAudio = true;
if (track.kind === 'video') hasLocalVideo = true;
});
}
// Keep remote receiving path alive even when local media permissions fail.
if (!hasLocalAudio) {
peerConnection.addTransceiver('audio', { direction: 'recvonly' });
}
if (!hasLocalVideo) {
peerConnection.addTransceiver('video', { direction: 'recvonly' });
}
// Apply candidates that arrived before peer connection was created.
if (pendingRemoteIceCandidates.length) {
const earlyCandidates = [...pendingRemoteIceCandidates];
pendingRemoteIceCandidates = [];
earlyCandidates.forEach((candidateData) => {
addOrQueueIceCandidate(candidateData).catch((error) => {
console.warn('Failed to apply early ICE candidate:', error);
});
});
}
peerConnection.onicecandidate = (event) => {
if (event.candidate && socket && socket.connected) {
socket.emit('video signal', {
toId: peerId,
callId: activeCallState?.callId,
signal: { type: 'ice-candidate', candidate: event.candidate }
});
}
};
peerConnection.ontrack = (event) => {
// Add the incoming track to our managed MediaStream so srcObject stays consistent.
if (event.track && remoteCallStream) {
if (!remoteCallStream.getTrackById(event.track.id)) {
remoteCallStream.addTrack(event.track);
appendCallLog(`Remote ${event.track.kind} track received`);
}
}
const remoteVideo = document.getElementById('remoteCallVideo');
if (remoteVideo) {
if (remoteVideo.srcObject !== remoteCallStream) {
remoteVideo.srcObject = remoteCallStream;
}
remoteVideo.muted = false;
remoteVideo.volume = 1.0;
ensureRemoteMediaPlayback(remoteVideo);
}
};
function showCallWarningThrottled(message) {
const now = Date.now();
if (now - lastCallWarningAt < 5000) return;
lastCallWarningAt = now;
showToast(message, 'warning');
}
peerConnection.onconnectionstatechange = () => {
console.log('📡 Connection state:', peerConnection.connectionState);
if (window.userIntentionallEndedCall) {
console.log('User ended call intentionally, skipping error messages');
return;
}
if (peerConnection.connectionState === 'connecting') {
setCallStatus('Connecting...');
showToast('📞 Connecting call...', 'info');
} else if (peerConnection.connectionState === 'connected') {
callIceFailureCount = 0;
setCallStatus('Connected');
appendCallLog('Peer connection connected');
showToast('📞 Call connected!', 'success');
// Start timer only if not already running (avoids resetting a running clock)
if (!callTimerInterval) {
startCallTimer();
}
// Ensure remote media plays after ICE recovery
const remoteVidEl = document.getElementById('remoteCallVideo');
if (remoteVidEl) {
if (!remoteVidEl.srcObject && remoteCallStream) {
remoteVidEl.srcObject = remoteCallStream;
}
remoteVidEl.muted = false;
remoteVidEl.volume = 1.0;
if (remoteVidEl.srcObject) {
ensureRemoteMediaPlayback(remoteVidEl);
}
}
} else if (peerConnection.connectionState === 'failed') {
setCallStatus('Connection failed');
appendCallLog('Peer connection failed, starting recovery');
showCallWarningThrottled('⚠️ Network issue detected, retrying call connection...');
attemptIceRecovery(peerId);
} else if (peerConnection.connectionState === 'closed') {
cleanupCallSession(false);
}
};
peerConnection.oniceconnectionstatechange = () => {
if (!peerConnection) return;
console.log('ICE connection state:', peerConnection.iceConnectionState);
appendCallLog(`ICE state: ${peerConnection.iceConnectionState}`);
if (peerConnection.iceConnectionState === 'connected' || peerConnection.iceConnectionState === 'completed') {
callIceFailureCount = 0;
if (pendingIceRecoveryTimer) {
clearTimeout(pendingIceRecoveryTimer);
pendingIceRecoveryTimer = null;
}
if (pendingRelayFallbackTimer) {
clearTimeout(pendingRelayFallbackTimer);
pendingRelayFallbackTimer = null;
}
return;
}
if (peerConnection.iceConnectionState === 'checking' && forceRelayOnly && !pendingRelayFallbackTimer) {
// If relay-only stalls, fall back to mixed ICE so STUN/P2P can connect.
pendingRelayFallbackTimer = setTimeout(() => {
pendingRelayFallbackTimer = null;
if (!peerConnection) return;
if (peerConnection.iceConnectionState !== 'checking') return;
try {
forceRelayOnly = false;
peerConnection.setConfiguration({
iceServers: activeIceServers,
iceTransportPolicy: 'all'
});
showToast('Retrying call with mixed network mode...', 'warning');
appendCallLog('Fallback from relay-only to mixed mode');
attemptIceRecovery(peerId);
} catch (fallbackError) {
console.warn('Relay fallback failed:', fallbackError);
}
}, 10000);
}
if (peerConnection.iceConnectionState === 'disconnected') {
if (pendingIceRecoveryTimer) {
clearTimeout(pendingIceRecoveryTimer);
}
// 'disconnected' can be temporary on mobile networks; wait before forcing ICE restart.
pendingIceRecoveryTimer = setTimeout(() => {
pendingIceRecoveryTimer = null;
if (!peerConnection) return;
if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'failed') {
showCallWarningThrottled('⚠️ Connection unstable, trying to reconnect...');
attemptIceRecovery(peerId);
}
}, 3000);
return;
}
if (peerConnection.iceConnectionState === 'failed') {
if (pendingIceRecoveryTimer) {
clearTimeout(pendingIceRecoveryTimer);
pendingIceRecoveryTimer = null;
}
attemptIceRecovery(peerId);
}
};
async function attemptIceRecovery(recoveryPeerId) {
if (!peerConnection || !socket || !socket.connected) return;
const now = Date.now();
if (now - lastIceRecoveryAttemptAt < 4000) return;
lastIceRecoveryAttemptAt = now;
callIceFailureCount += 1;
try {
if (callIceFailureCount >= 2 && !forceRelayOnly && hasTurnServersConfigured) {
forceRelayOnly = true;
peerConnection.setConfiguration({
iceServers: activeIceServers,
iceTransportPolicy: 'relay'
});
showToast('Switching call to relay mode for network compatibility...', 'warning');
appendCallLog('Switching to relay mode for recovery');
} else if (callIceFailureCount >= 2 && !hasTurnServersConfigured) {
showToast('Retrying call without relay fallback...', 'warning');
appendCallLog('No TURN available, retrying direct ICE only');
}
peerConnection.restartIce?.();
if (peerConnection.signalingState === 'stable') {
const recoveryOffer = await peerConnection.createOffer({
iceRestart: true,
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
await peerConnection.setLocalDescription(recoveryOffer);
appendCallLog('ICE restart offer created and sent');
socket.emit('video signal', {
toId: recoveryPeerId,
callId: activeCallState?.callId,
signal: { type: 'offer', offer: recoveryOffer }
});
}
} catch (recoveryError) {
console.warn('ICE recovery attempt failed:', recoveryError);
}
}
}
function openCallDialog(peerName) {
// Use the new full-screen call overlay instead of a custom dialog
const overlay = document.getElementById('callOverlay');
if (overlay) {
overlay.classList.toggle('desktop-window', window.innerWidth > 700);
callEventLog = [];
document.getElementById('callOverlayPeerName').textContent = `📞 ${peerName}`;
document.getElementById('callTimerLabel').textContent = '00:00:00';
setCallStatus('Preparing call...');
const muteBtn = document.getElementById('muteBtn');
if (muteBtn) { muteBtn.textContent = '🎤'; muteBtn.classList.remove('muted'); }
const camBtn = document.getElementById('cameraBtn');
if (camBtn) { camBtn.textContent = '📷'; camBtn.classList.remove('cam-off'); }
isCallCameraOff = false;
overlay.classList.add('active');
const dbg = document.getElementById('callDebugPanel');
if (dbg) dbg.style.display = 'block';
appendCallLog(`Call window opened for ${peerName}`);
}
}
async function startCallSession(peerName, peerId, isCaller) {
if (peerConnection && currentCallPeerId && String(currentCallPeerId) === String(peerId)) {
return;
}
if (isStartingCallSession) return;
isStartingCallSession = true;
try {
currentCallPeerId = peerId;
currentCallPeerName = peerName;
callIceFailureCount = 0;
await ensureRtcConfigLoaded();
forceRelayOnly = rtcRuntimeConfig?.iceTransportPolicy === 'relay';
appendCallLog(`Starting session as ${isCaller ? 'caller' : 'callee'}`);
openCallDialog(peerName);
// Start timer from session start to avoid "stuck at 00:00" perception on unstable networks.
if (!callTimerInterval) {
startCallTimer();
}
try {
await ensureLocalCallStream();
} catch (mediaError) {
// Do not abort the call - continue in receive-only mode so user can still hear/see remote party.
console.warn('Local media unavailable, continuing receive-only call:', mediaError);
localCallStream = null;
appendCallLog(`Local media unavailable: ${mediaError?.message || mediaError}`);
showToast('Starting call in receive-only mode (local camera/mic unavailable)', 'warning');
}
const localVideo = document.getElementById('localCallVideo');
if (localVideo && localCallStream) {
localVideo.srcObject = localCallStream;
ensureLocalPreviewPlayback(localVideo);
} else if (localVideo) {
localVideo.srcObject = null;
}
createPeerConnection(peerId);
appendCallLog('Peer connection created');
if (isCaller && peerConnection) {
makingOffer = true;
try {
const offer = await peerConnection.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
await peerConnection.setLocalDescription(offer);
appendCallLog('Offer created and sent');
socket.emit('video signal', {
toId: peerId,
callId: activeCallState?.callId,
signal: { type: 'offer', offer }
});
} finally {
makingOffer = false;
}
}
// Handle an offer that arrived while this session was being set up
if (!isCaller && pendingSignalOffer && peerConnection) {
const po = pendingSignalOffer;
pendingSignalOffer = null;
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(po.offer));
appendCallLog('Pending remote offer applied');
await flushQueuedIceCandidates();
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
appendCallLog('Answer created for pending offer');
socket.emit('video signal', {
toId: po.fromId,
callId: activeCallState?.callId,
signal: { type: 'answer', answer }
});
} catch (pendingOfferErr) {
console.error('Failed to handle pending offer:', pendingOfferErr);
}
}
} finally {
isStartingCallSession = false;
}
}
function endVideoCall() {
if (currentCallProvider === 'hosted' && currentJitsiApi) {
currentJitsiApi.executeCommand('hangup');
}
window.userIntentionallEndedCall = true;
if (socket && socket.connected && currentCallPeerId) {
socket.emit('end video call', {
callId: activeCallState?.callId,
toId: currentCallPeerId
});
}
cleanupCallSession(false);
}
function toggleLocalMute() {
if (currentCallProvider === 'hosted' && currentJitsiApi) {
currentJitsiApi.executeCommand('toggleAudio');
return;
}
if (!localCallStream) {
showToast('Microphone unavailable on this device/browser', 'warning');
return;
}
const audioTracks = localCallStream.getAudioTracks();
if (!audioTracks.length) {
showToast('No microphone track available', 'warning');
return;
}
const currentlyEnabled = audioTracks.some(track => track.enabled);
audioTracks.forEach(track => {
track.enabled = !currentlyEnabled;
});
const btn = document.getElementById('muteBtn');
if (btn) {
const nowMuted = currentlyEnabled;
btn.textContent = nowMuted ? '🔇' : '🎤';
if (nowMuted) btn.classList.add('muted'); else btn.classList.remove('muted');
}
showToast(currentlyEnabled ? '🔇 Microphone muted' : '🎤 Microphone on', 'info');
}
function toggleCallCamera() {
if (currentCallProvider === 'hosted' && currentJitsiApi) {
currentJitsiApi.executeCommand('toggleVideo');
return;
}
if (!localCallStream) {
showToast('Camera unavailable on this device/browser', 'warning');
return;
}
const videoTracks = localCallStream.getVideoTracks();
if (!videoTracks.length) {
showToast('No camera track available', 'warning');
return;
}
isCallCameraOff = !isCallCameraOff;
videoTracks.forEach(track => { track.enabled = !isCallCameraOff; });
const btn = document.getElementById('cameraBtn');
if (btn) {
btn.textContent = isCallCameraOff ? '🚫' : '📷';
if (isCallCameraOff) btn.classList.add('cam-off'); else btn.classList.remove('cam-off');
}
showToast(isCallCameraOff ? '📷 Camera off' : '📷 Camera on', 'info');
}
function toggleCallDebug() {
const dbg = document.getElementById('callDebugPanel');
if (!dbg) return;
if (dbg.style.display === 'block') {
dbg.style.display = 'none';
return;
}
dbg.style.display = 'block';
refreshCallDebug();
}
function refreshCallDebug() {
const dbg = document.getElementById('callDebugPanel');
if (!dbg || dbg.style.display !== 'block') return;
const connState = peerConnection ? peerConnection.connectionState : 'none';
const iceState = peerConnection ? peerConnection.iceConnectionState : 'none';
const sigState = peerConnection ? peerConnection.signalingState : 'none';
const localTracks = localCallStream ? localCallStream.getTracks().map(t => `${t.kind}[${t.enabled ? 'on' : 'off'}]`).join(', ') : 'none';
const remoteTracks = remoteCallStream ? remoteCallStream.getTracks().map(t => `${t.kind}[${t.readyState}]`).join(', ') : 'none';
const remoteVid = document.getElementById('remoteCallVideo');
const vidState = remoteVid ? `paused:${remoteVid.paused} muted:${remoteVid.muted} vol:${remoteVid.volume}` : 'none';
const socketStr = socket ? (socket.connected ? `connected [${socket.id}]` : 'disconnected') : 'null';
dbg.innerHTML = `
🔗 Connection: ${connState}
❄️ ICE State: ${iceState}
📡 Signaling: ${sigState}
🎤 Local Tracks: ${localTracks}
🔊 Remote Tracks: ${remoteTracks}
📺 Remote Video: ${vidState}
🔌 Socket: ${socketStr}
⏱ Call duration: ${document.getElementById('callTimerLabel')?.textContent || 'n/a'}
📋 Status: ${document.getElementById('callStatusLabel')?.textContent || 'n/a'}
${callEventLog.map((entry) => `
${entry}
`).join('')}
`;
}
function forceRestartIce() {
if (!peerConnection || !currentCallPeerId) {
showToast('No active call to restart ICE', 'warning');
return;
}
peerConnection.restartIce?.();
showToast('♻️ ICE restart triggered', 'info');
refreshCallDebug();
}
function cleanupCallSession(keepDialogOpen = false) {
window.userIntentionallEndedCall = false;
stopIncomingCallAlert();
stopCallTimer();
if (currentJitsiApi) {
try {
currentJitsiApi.dispose();
} catch (_) {
}
currentJitsiApi = null;
}
currentCallProvider = 'webrtc';
currentCallRoomName = null;
setHostedCallVisible(false);
if (peerConnection) {
peerConnection.onicecandidate = null;
peerConnection.ontrack = null;
peerConnection.onconnectionstatechange = null;
peerConnection.oniceconnectionstatechange = null;
peerConnection.close();
peerConnection = null;
}
remoteIceCandidatesQueue = [];
remoteCallStream = null;
remoteCallPlaybackRetryBound = false;
remoteCallPlaybackStarted = false;
lastIceRecoveryAttemptAt = 0;
lastCallWarningAt = 0;
isStartingCallSession = false;
pendingSignalOffer = null;
makingOffer = false;
callIceFailureCount = 0;
forceRelayOnly = false;
activeIceServers = [];
if (pendingIceRecoveryTimer) {
clearTimeout(pendingIceRecoveryTimer);
pendingIceRecoveryTimer = null;
}
if (pendingRelayFallbackTimer) {
clearTimeout(pendingRelayFallbackTimer);
pendingRelayFallbackTimer = null;
}
pendingRemoteIceCandidates = [];
if (localCallStream) {
localCallStream.getTracks().forEach((track) => track.stop());
localCallStream = null;
}
currentCallPeerId = null;
currentCallPeerName = null;
activeCallState = null;
callEventLog = [];
setCallStatus('');
// Close the overlay
const overlay = document.getElementById('callOverlay');
if (overlay) overlay.classList.remove('active');
// Also close any old dialog if still open
if (!keepDialogOpen) {
closeCustomDialog();
}
// Clear debug panel
const dbg = document.getElementById('callDebugPanel');
if (dbg) { dbg.style.display = 'none'; dbg.innerHTML = ''; }
}
function visitUserProfile(userId, userName = 'User') {
if (!userId || userId === 'undefined') {
showToast('❌ Invalid user ID', 'error');
return;
}
const isOnline = (globalOnlineUsers || []).some((u) => String(u?.id || u?.userId || u?._id || '') === String(userId));
const dict = getUiDictionary(currentLanguage);
const onlineText = isOnline ? translateRawText('Online', dict) : translateRawText('Offline', dict);
const statusColor = isOnline ? 'var(--success)' : 'var(--text-secondary)';
const html = `
${userName.charAt(0).toUpperCase()}
${userName}
${translateRawText('Status', dict)}: ● ${onlineText}
`;
customAlert(html, `👤 ${userName}'s Profile`, '👤');
localizeStaticUi(currentLanguage);
}
function sendFriendRequest(userId, userName = 'User') {
customConfirm(
`Send friend request to ${userName}?`,
'👥 Add Friend',
'👥',
false
).then(async confirmed => {
if (!confirmed) return;
try {
const response = await fetch(`${API_BASE}/friends/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ toUserId: userId })
});
if (response.ok) {
showToast(`✅ Friend request sent to ${userName}!`, 'success');
} else {
const data = await response.json();
showToast(data.error || 'Failed to send friend request', 'error');
}
} catch (error) {
console.error('Error sending friend request:', error);
showToast('❌ Could not send friend request', 'error');
}
});
}
let copiedMediaItems = [];
function addToMediaPreview(type, url) {
copiedMediaItems.push({ type, url, timestamp: Date.now() });
renderMediaPreview();
showToast('✅ Media added to preview!', 'success');
}
function renderMediaPreview() {
const content = document.getElementById('mediaPreviewContent');
if (!content) return;
content.innerHTML = '';
if (copiedMediaItems.length === 0) {
content.innerHTML = `
No saved media yet. Use message actions like copy/archive on images or videos to add them here.
`;
return;
}
copiedMediaItems.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'media-preview-item';
if (item.type === 'image') {
const img = document.createElement('img');
img.src = item.url;
img.alt = 'Copied image';
div.appendChild(img);
} else if (item.type === 'video') {
const video = document.createElement('video');
video.src = item.url;
video.controls = true;
div.appendChild(video);
}
content.appendChild(div);
});
}
function openMediaPreview() {
renderMediaPreview();
document.getElementById('mediaPreviewPanel').classList.add('show');
}
function closeMediaPreview() {
document.getElementById('mediaPreviewPanel').classList.remove('show');
}
function filterFriends(searchText) {
const channelsList = document.getElementById('channelsList');
const channels = channelsList.querySelectorAll('.channel-item');
searchText = searchText.toLowerCase().trim();
channels.forEach(channel => {
const username = channel.textContent.toLowerCase();
if (username.includes(searchText)) {
channel.style.display = 'flex';
} else {
channel.style.display = 'none';
}
});
}
let archivedMessages = JSON.parse(localStorage.getItem('archivedMessages') || '[]');
function archiveSingleMessage(messageId, messageDiv) {
const textElement = messageDiv.querySelector('.message-text');
const imgElement = messageDiv.querySelector('img');
const vidElement = messageDiv.querySelector('video');
const messageData = {
id: messageId,
text: textElement?.textContent || '',
mediaUrl: imgElement?.src || vidElement?.src || null,
mediaType: imgElement ? 'image' : vidElement ? 'video' : 'text',
timestamp: Date.now(),
sender: messageDiv.dataset.sender || 'Unknown'
};
archivedMessages.push(messageData);
localStorage.setItem('archivedMessages', JSON.stringify(archivedMessages));
messageDiv.style.transition = 'all 0.4s ease';
messageDiv.style.opacity = '0.5';
messageDiv.style.transform = 'translateX(-20px)';
showToast('📦 Message archived!', 'success');
setTimeout(() => {
messageDiv.style.opacity = '1';
messageDiv.style.transform = 'translateX(0)';
}, 500);
}
function showArchivedMessages() {
const modal = document.getElementById('archivedChatsModal');
const list = document.getElementById('archivedChatsList');
if (!modal || !list) {
showToast('Archive UI unavailable', 'error');
return;
}
const chatsHtml = archivedChats.map((chat) => {
const icon = chat.type === 'dm' ? '👤' : chat.type === 'community' ? '🌐' : '👥';
const safeName = String(chat.name || '').replace(/'/g, "\\'");
return `
${icon}
${chat.name}
${chat.type}
`;
}).join('');
const messageItems = archivedMessages.slice().reverse().slice(0, 80).map((m) => {
const preview = String(m.text || '[Media message]').replace(//g, '>');
return `
${new Date(m.timestamp).toLocaleString()}
${preview}
`;
}).join('');
list.innerHTML = `
Archived chats: ${archivedChats.length} | Archived single messages: ${archivedMessages.length}
${archivedChats.length > 0 ? `Chats
${chatsHtml}` : ''}
${archivedMessages.length > 0 ? `Single Messages
${messageItems}` : ''}
${archivedChats.length === 0 && archivedMessages.length === 0 ? 'No archived content yet.
' : ''}
`;
modal.classList.remove('hidden');
}
async function clearCurrentChatHistory() {
if (!currentChatContext || currentChatContext.type === 'none') {
showToast('Open a chat first', 'warning');
return false;
}
const confirmed = await customConfirm(
'This will permanently clear all messages in the current chat for your account.',
'Clear Chat History',
'🧹',
true
);
if (!confirmed) return false;
try {
if (currentChatContext.type === 'dm') {
const response = await fetch(`${API_BASE}/dms/${currentChatContext.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${currentToken}` }
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
showToast(data.error || 'Failed to clear chat history', 'error');
return false;
}
} else {
socket?.emit('clear room chat', {
roomType: currentChatContext.type,
roomName: currentChatContext.name
});
const roomKey = normalizeRoomKeyClient(currentChatContext.type, currentChatContext.name);
roomMessages[roomKey] = [];
saveRoomMessages();
}
document.getElementById('messages-container').innerHTML = '';
refreshChatScrollbar();
showToast('Chat history cleared', 'success');
return true;
} catch (error) {
console.error(error);
showToast('Failed to clear chat history', 'error');
return false;
}
}
async function rejectFriendRequest(requestId) {
try {
const response = await fetch(`${API_BASE}/friends/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({ requestId })
});
const data = await response.json();
if (!response.ok) {
showToast(data.error || 'Failed to decline friend request', 'error');
return;
}
showToast('Friend request declined', 'warning');
loadFriends();
} catch (error) {
console.error(error);
showToast('Could not decline friend request', 'error');
}
}