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

Choose a preset or upload your own image.

${presetsHtml}
`; 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' ? '🌐' : '👥'}
${roomName} Now
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 `
${avatarHtml}
${c.name}
${c.description}
${isJoined ? `` : ''}
`; }).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) => `
${String(m.username || 'U').charAt(0).toUpperCase()}
${m.username} View full profile
${m.role === 'leader' ? 'Leader' : 'Member'}
`).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}
Or paste any emoji:
`; 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 = `
${icon}
${title}
${message}
`; 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 = `
${icon}
${title}
${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 = `
${icon}
${title}
${message}
`; 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'); } }