import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useQuery, useConvex } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Avatar from './Avatar'; import AvatarCropModal from './AvatarCropModal'; import { useTheme, THEMES, THEME_LABELS } from '../contexts/ThemeContext'; import { useVoice } from '../contexts/VoiceContext'; import { useSearch } from '../contexts/SearchContext'; import { usePlatform } from '../platform'; const THEME_PREVIEWS = { [THEMES.LIGHT]: { bg: '#ffffff', sidebar: '#f2f3f5', tertiary: '#e3e5e8', text: '#313338' }, [THEMES.DARK]: { bg: '#313338', sidebar: '#2b2d31', tertiary: '#1e1f22', text: '#f2f3f5' }, [THEMES.ASH]: { bg: '#202225', sidebar: '#1a1b1e', tertiary: '#111214', text: '#f0f1f3' }, [THEMES.ONYX]: { bg: '#0c0c14', sidebar: '#080810', tertiary: '#000000', text: '#e0def0' }, }; const TABS = [ { id: 'account', label: 'My Account', section: 'USER SETTINGS' }, { id: 'security', label: 'Security', section: 'USER SETTINGS' }, { id: 'appearance', label: 'Appearance', section: 'USER SETTINGS' }, { id: 'voice', label: 'Voice & Video', section: 'APP SETTINGS' }, { id: 'keybinds', label: 'Keybinds', section: 'APP SETTINGS' }, { id: 'search', label: 'Search', section: 'APP SETTINGS' }, ]; const UserSettings = ({ onClose, userId, username, onLogout }) => { const [activeTab, setActiveTab] = useState('account'); useEffect(() => { const handleKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [onClose]); const renderSidebar = () => { let lastSection = null; const items = []; TABS.forEach((tab, i) => { if (tab.section !== lastSection) { if (lastSection !== null) { items.push(
); } items.push(
{tab.section}
); lastSection = tab.section; } items.push(
setActiveTab(tab.id)} style={{ padding: '6px 10px', borderRadius: '4px', cursor: 'pointer', marginBottom: '2px', fontSize: '15px', backgroundColor: activeTab === tab.id ? 'var(--background-modifier-selected)' : 'transparent', color: activeTab === tab.id ? 'var(--header-primary)' : 'var(--header-secondary)', }} > {tab.label}
); }); items.push(
); items.push(
Log Out
); return items; }; return (
{/* Sidebar */}
{renderSidebar()}
{/* Content */}
{activeTab === 'account' && } {activeTab === 'security' && } {activeTab === 'appearance' && } {activeTab === 'voice' && } {activeTab === 'keybinds' && } {activeTab === 'search' && }
{/* Right spacer with close button */}
ESC
); }; /* ========================================= MY ACCOUNT TAB ========================================= */ const MyAccountTab = ({ userId, username }) => { const allUsers = useQuery(api.auth.getPublicKeys); const convex = useConvex(); const currentUser = allUsers?.find(u => u.id === userId); const [displayName, setDisplayName] = useState(''); const [aboutMe, setAboutMe] = useState(''); const [customStatus, setCustomStatus] = useState(''); const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(null); const [saving, setSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [showCropModal, setShowCropModal] = useState(false); const [rawImageUrl, setRawImageUrl] = useState(null); const fileInputRef = useRef(null); const [joinSoundFile, setJoinSoundFile] = useState(null); const [joinSoundPreviewName, setJoinSoundPreviewName] = useState(null); const [removeJoinSound, setRemoveJoinSound] = useState(false); const joinSoundInputRef = useRef(null); const joinSoundAudioRef = useRef(null); useEffect(() => { if (currentUser) { setDisplayName(currentUser.displayName || ''); setAboutMe(currentUser.aboutMe || ''); setCustomStatus(currentUser.customStatus || ''); } }, [currentUser]); useEffect(() => { if (!currentUser) return; const changed = displayName !== (currentUser.displayName || '') || aboutMe !== (currentUser.aboutMe || '') || customStatus !== (currentUser.customStatus || '') || avatarFile !== null || joinSoundFile !== null || removeJoinSound; setHasChanges(changed); }, [displayName, aboutMe, customStatus, avatarFile, joinSoundFile, removeJoinSound, currentUser]); const handleAvatarChange = (e) => { const file = e.target.files?.[0]; if (!file) return; const url = URL.createObjectURL(file); setRawImageUrl(url); setShowCropModal(true); e.target.value = ''; }; const handleCropApply = (blob) => { const file = new File([blob], 'avatar.png', { type: 'image/png' }); setAvatarFile(file); const previewUrl = URL.createObjectURL(blob); setAvatarPreview(previewUrl); if (rawImageUrl) URL.revokeObjectURL(rawImageUrl); setRawImageUrl(null); setShowCropModal(false); }; const handleCropCancel = () => { if (rawImageUrl) URL.revokeObjectURL(rawImageUrl); setRawImageUrl(null); setShowCropModal(false); }; const handleJoinSoundChange = (e) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 10 * 1024 * 1024) { alert('Join sound must be under 10MB'); e.target.value = ''; return; } setJoinSoundFile(file); setJoinSoundPreviewName(file.name); setRemoveJoinSound(false); e.target.value = ''; }; const handleJoinSoundPreview = () => { if (joinSoundAudioRef.current) { joinSoundAudioRef.current.pause(); joinSoundAudioRef.current = null; } let src; if (joinSoundFile) { src = URL.createObjectURL(joinSoundFile); } else if (currentUser?.joinSoundUrl) { src = currentUser.joinSoundUrl; } if (src) { const audio = new Audio(src); audio.volume = 0.5; audio.play().catch(e => console.error('Preview failed', e)); joinSoundAudioRef.current = audio; } }; const handleRemoveJoinSound = () => { setJoinSoundFile(null); setJoinSoundPreviewName(null); setRemoveJoinSound(true); }; const handleSave = async () => { if (!userId || saving) return; setSaving(true); try { let avatarStorageId; if (avatarFile) { const uploadUrl = await convex.mutation(api.files.generateUploadUrl); const res = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Type': avatarFile.type }, body: avatarFile, }); const { storageId } = await res.json(); avatarStorageId = storageId; } let joinSoundStorageId; if (joinSoundFile) { const jsUploadUrl = await convex.mutation(api.files.generateUploadUrl); const jsRes = await fetch(jsUploadUrl, { method: 'POST', headers: { 'Content-Type': joinSoundFile.type }, body: joinSoundFile, }); const jsData = await jsRes.json(); joinSoundStorageId = jsData.storageId; } const args = { userId, displayName, aboutMe, customStatus }; if (avatarStorageId) args.avatarStorageId = avatarStorageId; if (joinSoundStorageId) args.joinSoundStorageId = joinSoundStorageId; if (removeJoinSound) args.removeJoinSound = true; await convex.mutation(api.auth.updateProfile, args); setAvatarFile(null); setJoinSoundFile(null); setJoinSoundPreviewName(null); setRemoveJoinSound(false); if (avatarPreview) { URL.revokeObjectURL(avatarPreview); setAvatarPreview(null); } } catch (err) { console.error('Failed to save profile:', err); alert('Failed to save profile: ' + err.message); } finally { setSaving(false); } }; const handleReset = () => { if (currentUser) { setDisplayName(currentUser.displayName || ''); setAboutMe(currentUser.aboutMe || ''); setCustomStatus(currentUser.customStatus || ''); } setAvatarFile(null); setJoinSoundFile(null); setJoinSoundPreviewName(null); setRemoveJoinSound(false); if (avatarPreview) { URL.revokeObjectURL(avatarPreview); setAvatarPreview(null); } if (rawImageUrl) { URL.revokeObjectURL(rawImageUrl); setRawImageUrl(null); } setShowCropModal(false); }; const avatarUrl = avatarPreview || currentUser?.avatarUrl; return (

My Account

{/* Profile card */}
{/* Banner */}
{/* Profile body */}
{/* Avatar */}
fileInputRef.current?.click()} style={{ marginTop: '-40px', marginBottom: '12px', width: 'fit-content', cursor: 'pointer', position: 'relative' }} >
CHANGE
AVATAR
{/* Fields */}
{/* Username (read-only) */}
{username}
{/* Display Name */}
setDisplayName(e.target.value)} placeholder="How others see you in chat" style={{ width: '100%', backgroundColor: 'var(--bg-tertiary)', border: 'none', borderRadius: '4px', padding: '10px', color: 'var(--text-normal)', fontSize: '16px', outline: 'none', boxSizing: 'border-box', }} />
{/* About Me */}