From cb4361da1a6d670d90e687dab64b6a2861f34521 Mon Sep 17 00:00:00 2001 From: Bryan1029384756 <23323626+Bryan1029384756@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:36:40 -0600 Subject: [PATCH] feat: Implement core Discord features including members list, direct messages, user presence, authentication, and chat UI. --- .claude/settings.local.json | 6 +- Frontend/Electron/main.cjs | 373 +++++++-- Frontend/Electron/package-lock.json | 47 +- Frontend/Electron/package.json | 15 +- Frontend/Electron/preload.cjs | 11 + Frontend/Electron/src/App.jsx | 98 ++- Frontend/Electron/src/assets/icons/crown.svg | 1 + .../Electron/src/assets/icons/invite_user.svg | 1 + .../src/components/AvatarCropModal.jsx | 134 ++++ Frontend/Electron/src/components/ChatArea.jsx | 80 +- Frontend/Electron/src/components/DMList.jsx | 8 +- .../Electron/src/components/FriendsView.jsx | 15 +- .../Electron/src/components/MembersList.jsx | 11 +- Frontend/Electron/src/components/Sidebar.jsx | 79 +- .../Electron/src/components/UserSettings.jsx | 712 ++++++++++++++++++ .../Electron/src/contexts/PresenceContext.jsx | 47 ++ .../Electron/src/contexts/ThemeContext.jsx | 12 + Frontend/Electron/src/index.css | 275 ++++++- Frontend/Electron/src/pages/Chat.jsx | 49 +- Frontend/Electron/src/pages/Login.jsx | 16 + Frontend/Electron/src/styles/themes.css | 16 +- TODO.md | 17 +- convex/_generated/api.d.ts | 67 +- convex/auth.ts | 6 +- convex/convex.config.ts | 6 + convex/dms.ts | 2 +- convex/members.ts | 2 +- convex/presence.ts | 32 + convex/schema.ts | 1 + .../Settings Panel/settings snippit.txt | 3 + package-lock.json | 52 ++ package.json | 1 + 32 files changed, 2051 insertions(+), 144 deletions(-) create mode 100644 Frontend/Electron/src/assets/icons/crown.svg create mode 100644 Frontend/Electron/src/assets/icons/invite_user.svg create mode 100644 Frontend/Electron/src/components/AvatarCropModal.jsx create mode 100644 Frontend/Electron/src/components/UserSettings.jsx create mode 100644 Frontend/Electron/src/contexts/PresenceContext.jsx create mode 100644 convex/convex.config.ts create mode 100644 convex/presence.ts create mode 100644 discord-html-copy/Settings Panel/settings snippit.txt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bd4cce5..9146dd6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -19,7 +19,11 @@ "WebFetch(domain:github.com)", "WebFetch(domain:docs.gitea.com)", "WebFetch(domain:www.electron.build)", - "WebFetch(domain:forum.gitea.com)" + "WebFetch(domain:forum.gitea.com)", + "WebFetch(domain:www.convex.dev)", + "WebFetch(domain:www.npmjs.com)", + "WebFetch(domain:stack.convex.dev)", + "WebFetch(domain:raw.githubusercontent.com)" ] } } diff --git a/Frontend/Electron/main.cjs b/Frontend/Electron/main.cjs index 2457374..4ed46bb 100644 --- a/Frontend/Electron/main.cjs +++ b/Frontend/Electron/main.cjs @@ -1,13 +1,60 @@ -const { app, BrowserWindow, ipcMain, shell } = require('electron'); +const { app, BrowserWindow, ipcMain, shell, screen, safeStorage } = require('electron'); const path = require('path'); +const fs = require('fs'); + +// --- Secure session persistence --- +const SESSION_FILE = path.join(app.getPath('userData'), 'secure-session.dat'); const https = require('https'); const http = require('http'); const { checkForUpdates } = require('./updater.cjs'); +// --- Settings persistence --- +const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json'); +const DEFAULT_SETTINGS = { + windowX: undefined, + windowY: undefined, + windowWidth: 1200, + windowHeight: 800, + isMaximized: false, + theme: 'theme-dark', +}; + +let mainWindow = null; + +function loadSettings() { + try { + const data = fs.readFileSync(SETTINGS_FILE, 'utf8'); + return { ...DEFAULT_SETTINGS, ...JSON.parse(data) }; + } catch { + return { ...DEFAULT_SETTINGS }; + } +} + +function saveSettings(settings) { + try { + fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8'); + } catch (err) { + console.error('Failed to save settings:', err.message); + } +} + +function isPositionOnScreen(x, y, w, h) { + const displays = screen.getAllDisplays(); + const MIN_OVERLAP = 100; + return displays.some(display => { + const { x: dx, y: dy, width: dw, height: dh } = display.bounds; + const overlapX = Math.max(0, Math.min(x + w, dx + dw) - Math.max(x, dx)); + const overlapY = Math.max(0, Math.min(y + h, dy + dh) - Math.max(y, dy)); + return overlapX >= MIN_OVERLAP && overlapY >= MIN_OVERLAP; + }); +} + function createWindow() { - const win = new BrowserWindow({ - width: 1200, - height: 800, + const settings = loadSettings(); + + const windowOptions = { + width: settings.windowWidth, + height: settings.windowHeight, frame: false, webPreferences: { nodeIntegration: false, @@ -15,17 +62,44 @@ function createWindow() { preload: path.join(__dirname, 'preload.cjs'), sandbox: false } + }; + + // Only restore position if it's valid on a connected display + if (settings.windowX !== undefined && settings.windowY !== undefined && + isPositionOnScreen(settings.windowX, settings.windowY, settings.windowWidth, settings.windowHeight)) { + windowOptions.x = settings.windowX; + windowOptions.y = settings.windowY; + } + + mainWindow = new BrowserWindow(windowOptions); + + if (settings.isMaximized) { + mainWindow.maximize(); + } + + // Save window state on close + mainWindow.on('close', () => { + const current = loadSettings(); // re-read to preserve theme changes + if (!mainWindow.isMaximized()) { + const bounds = mainWindow.getBounds(); + current.windowX = bounds.x; + current.windowY = bounds.y; + current.windowWidth = bounds.width; + current.windowHeight = bounds.height; + } + current.isMaximized = mainWindow.isMaximized(); + saveSettings(current); }); const isDev = process.env.npm_lifecycle_event === 'electron:dev'; if (isDev) { - win.loadURL('http://localhost:5173'); - win.webContents.openDevTools(); + mainWindow.loadURL('http://localhost:5173'); + mainWindow.webContents.openDevTools(); } else { // Production: Load the built file // dist-react is in the same directory as main.cjs - win.loadFile(path.join(__dirname, 'dist-react', 'index.html')); + mainWindow.loadFile(path.join(__dirname, 'dist-react', 'index.html')); } } @@ -77,75 +151,254 @@ app.whenReady().then(async () => { }); // Helper to fetch metadata (Zero-Knowledge: Client fetches previews) + + const OEMBED_PROVIDERS = { + 'twitter.com': (url) => url.includes('/status/') ? `https://publish.twitter.com/oembed?url=${encodeURIComponent(url)}&format=json` : null, + 'x.com': (url) => url.includes('/status/') ? `https://publish.twitter.com/oembed?url=${encodeURIComponent(url)}&format=json` : null, + 'youtube.com': (url) => `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`, + 'www.youtube.com': (url) => `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`, + 'youtu.be': (url) => `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`, + 'open.spotify.com': (url) => `https://open.spotify.com/oembed?url=${encodeURIComponent(url)}`, + 'www.tiktok.com': (url) => `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`, + 'tiktok.com': (url) => `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`, + 'www.reddit.com': (url) => `https://www.reddit.com/oembed?url=${encodeURIComponent(url)}&format=json`, + 'reddit.com': (url) => `https://www.reddit.com/oembed?url=${encodeURIComponent(url)}&format=json`, + }; + + const FETCH_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (compatible; DiscordBot/1.0)', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + }; + + const MAX_RESPONSE_SIZE = 256 * 1024; // 256KB + const FETCH_TIMEOUT = 8000; + const MAX_REDIRECTS = 5; + + function httpGet(url, redirectsLeft = MAX_REDIRECTS) { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http; + const req = client.get(url, { headers: FETCH_HEADERS, timeout: FETCH_TIMEOUT }, (res) => { + if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) { + if (redirectsLeft <= 0) { resolve(''); return; } + let redirectUrl = res.headers.location; + if (redirectUrl.startsWith('/')) { + const parsed = new URL(url); + redirectUrl = parsed.origin + redirectUrl; + } + res.resume(); + httpGet(redirectUrl, redirectsLeft - 1).then(resolve).catch(reject); + return; + } + let data = ''; + let size = 0; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + size += Buffer.byteLength(chunk); + if (size > MAX_RESPONSE_SIZE) { res.destroy(); resolve(data); return; } + data += chunk; + }); + res.on('end', () => resolve(data)); + res.on('error', () => resolve(data)); + }); + req.on('timeout', () => { req.destroy(); resolve(''); }); + req.on('error', (err) => { console.error('httpGet error:', err.message); resolve(''); }); + }); + } + + function fetchJson(url, redirectsLeft = MAX_REDIRECTS) { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http; + const req = client.get(url, { headers: { 'User-Agent': FETCH_HEADERS['User-Agent'], 'Accept': 'application/json' }, timeout: FETCH_TIMEOUT }, (res) => { + if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) { + if (redirectsLeft <= 0) { resolve(null); return; } + let redirectUrl = res.headers.location; + if (redirectUrl.startsWith('/')) { + const parsed = new URL(url); + redirectUrl = parsed.origin + redirectUrl; + } + res.resume(); + fetchJson(redirectUrl, redirectsLeft - 1).then(resolve).catch(reject); + return; + } + let data = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(null); } }); + res.on('error', () => resolve(null)); + }); + req.on('timeout', () => { req.destroy(); resolve(null); }); + req.on('error', () => resolve(null)); + }); + } + + function parseMetaTags(html) { + const meta = {}; + // Match both orderings: property/name before content AND content before property/name + const metaRegex = /]*?)\/?\s*>/gi; + let match; + while ((match = metaRegex.exec(html)) !== null) { + const attrs = match[1]; + let name = null, content = null; + // Extract property or name attribute + const propMatch = attrs.match(/(?:property|name)\s*=\s*["']([^"']+)["']/i); + const contentMatch = attrs.match(/content\s*=\s*["']([^"']*?)["']/i); + if (propMatch) name = propMatch[1].toLowerCase(); + if (contentMatch) content = contentMatch[1]; + if (name && content !== null) meta[name] = content; + } + // Extract tag + const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i); + if (titleMatch) meta['_title'] = titleMatch[1].trim(); + return meta; + } + + function buildMetadata(meta) { + const get = (...keys) => { for (const k of keys) { if (meta[k]) return meta[k]; } return null; }; + return { + title: get('og:title', 'twitter:title', '_title'), + description: get('og:description', 'twitter:description', 'description'), + image: get('og:image', 'og:image:secure_url', 'twitter:image', 'twitter:image:src'), + siteName: get('og:site_name'), + themeColor: get('theme-color'), + video: get('og:video:secure_url', 'og:video:url', 'og:video'), + type: get('og:type', 'twitter:card'), + author: get('article:author', 'author'), + }; + } + + function sanitizeMetadata(metadata) { + const decodeEntities = (str) => { + if (!str) return str; + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'") + .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(n)) + .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16))); + }; + const stripTags = (str) => str ? str.replace(/<[^>]*>/g, '') : str; + const limit = (str, max = 1000) => str && str.length > max ? str.substring(0, max) : str; + + const result = {}; + for (const [key, value] of Object.entries(metadata)) { + if (typeof value === 'string') { + result[key] = limit(decodeEntities(stripTags(value)).trim()); + } else { + result[key] = value; + } + } + return result; + } + ipcMain.handle('fetch-metadata', async (event, url) => { - return new Promise((resolve) => { - // Check for direct video links to avoid downloading large files + try { + // Check for direct video links const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov']; const imageExtensions = ['.gif', '.png', '.jpg', '.jpeg', '.webp']; const lowerUrl = url.toLowerCase(); - + if (videoExtensions.some(ext => lowerUrl.endsWith(ext))) { - const filename = url.split('/').pop(); - resolve({ - title: filename, - siteName: new URL(url).hostname, - video: url, - image: null, // No thumbnail for now unless we generate one - description: 'Video File' - }); - return; + return { title: url.split('/').pop(), siteName: new URL(url).hostname, video: url, image: null, description: 'Video File' }; } - if (imageExtensions.some(ext => lowerUrl.endsWith(ext))) { - const filename = url.split('/').pop(); - resolve({ - title: filename, - siteName: new URL(url).hostname, - video: null, - image: url, // Direct image/gif - description: 'Image File' - }); - return; + return { title: url.split('/').pop(), siteName: new URL(url).hostname, video: null, image: url, description: 'Image File' }; } - const client = url.startsWith('https') ? https : http; - const req = client.get(url, (res) => { - let data = ''; - res.on('data', (chunk) => data += chunk); - res.on('end', () => { - // Simple Regex Parser for OG Tags to avoid dependencies - const getMeta = (prop) => { - const regex = new RegExp(`<meta\\s+(?:name|property)=["'](?:og:)?${prop}["']\\s+content=["'](.*?)["']`, 'i'); - const match = data.match(regex); - return match ? match[1] : null; - }; - const getTitle = () => { - const regex = /<title>(.*?)<\/title>/i; - const match = data.match(regex); - return match ? match[1] : null; - }; + const hostname = new URL(url).hostname.replace(/^www\./, ''); + const oembedBuilder = OEMBED_PROVIDERS[hostname] || OEMBED_PROVIDERS['www.' + hostname]; + const oembedUrl = oembedBuilder ? oembedBuilder(url) : null; - const metadata = { - title: getMeta('title') || getTitle(), - description: getMeta('description'), - image: getMeta('image'), - siteName: getMeta('site_name'), - themeColor: getMeta('theme-color') - }; - resolve(metadata); - }); - }); - req.on('error', (err) => { - console.error('Metadata fetch error:', err); - resolve(null); - }); - }); + let metadata; + + if (oembedUrl) { + // Fetch oEmbed JSON and page HTML in parallel + const [oembedResult, htmlResult] = await Promise.allSettled([ + fetchJson(oembedUrl), + httpGet(url), + ]); + + const oembed = oembedResult.status === 'fulfilled' ? oembedResult.value : null; + const html = htmlResult.status === 'fulfilled' ? htmlResult.value : ''; + const meta = html ? parseMetaTags(html) : {}; + const ogData = buildMetadata(meta); + + metadata = { + title: oembed?.title || ogData.title, + description: ogData.description, + image: oembed?.thumbnail_url || ogData.image, + siteName: oembed?.provider_name || ogData.siteName, + themeColor: ogData.themeColor, + video: ogData.video, + type: ogData.type, + author: oembed?.author_name || ogData.author, + }; + } else { + // Standard HTML fetch + const html = await httpGet(url); + if (!html) return null; + const meta = parseMetaTags(html); + metadata = buildMetadata(meta); + } + + return sanitizeMetadata(metadata); + } catch (err) { + console.error('Metadata fetch error:', err); + return null; + } }); ipcMain.handle('open-external', async (event, url) => { await shell.openExternal(url); }); + // Settings IPC handlers + ipcMain.handle('get-setting', (event, key) => { + const settings = loadSettings(); + return settings[key]; + }); + + ipcMain.handle('set-setting', (event, key, value) => { + const settings = loadSettings(); + settings[key] = value; + saveSettings(settings); + }); + + // Secure session persistence handlers + ipcMain.handle('save-session', (event, data) => { + try { + if (!safeStorage.isEncryptionAvailable()) return false; + const encrypted = safeStorage.encryptString(JSON.stringify(data)); + fs.writeFileSync(SESSION_FILE, encrypted); + return true; + } catch (err) { + console.error('Failed to save session:', err.message); + return false; + } + }); + + ipcMain.handle('load-session', () => { + try { + if (!safeStorage.isEncryptionAvailable()) return null; + if (!fs.existsSync(SESSION_FILE)) return null; + const encrypted = fs.readFileSync(SESSION_FILE); + const decrypted = safeStorage.decryptString(encrypted); + return JSON.parse(decrypted); + } catch (err) { + console.error('Failed to load session (clearing corrupt file):', err.message); + try { fs.unlinkSync(SESSION_FILE); } catch {} + return null; + } + }); + + ipcMain.handle('clear-session', () => { + try { + if (fs.existsSync(SESSION_FILE)) fs.unlinkSync(SESSION_FILE); + return true; + } catch (err) { + console.error('Failed to clear session:', err.message); + return false; + } + }); + ipcMain.handle('get-screen-sources', async () => { const { desktopCapturer } = require('electron'); const sources = await desktopCapturer.getSources({ diff --git a/Frontend/Electron/package-lock.json b/Frontend/Electron/package-lock.json index ee8169a..7865304 100644 --- a/Frontend/Electron/package-lock.json +++ b/Frontend/Electron/package-lock.json @@ -1,13 +1,14 @@ { "name": "discord", - "version": "0.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "discord", - "version": "0.0.0", + "version": "1.0.2", "dependencies": { + "@convex-dev/presence": "^0.3.0", "@livekit/components-react": "^2.9.17", "@livekit/components-styles": "^1.2.0", "convex": "^1.31.2", @@ -16,6 +17,7 @@ "livekit-client": "^2.16.1", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-easy-crop": "^5.5.6", "react-markdown": "^10.1.0", "react-router-dom": "^7.11.0", "react-syntax-highlighter": "^16.1.0", @@ -334,6 +336,27 @@ "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@convex-dev/presence": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@convex-dev/presence/-/presence-0.3.0.tgz", + "integrity": "sha512-adV+ao1L77u+egobyJabNwuai/0y/VgzMbqZiy+Q49JmX6fbSaiYg6FpFVACqPaq3giOfjrN2k+5mK2jTAUG0g==", + "license": "Apache-2.0", + "peerDependencies": { + "convex": "^1.24.8", + "expo-crypto": ">=14.1.0", + "react": "~18.3.1 || ^19.0.0", + "react-dom": "~18.3.1 || ^19.0.0", + "react-native": ">=0.79.0" + }, + "peerDependenciesMeta": { + "expo-crypto": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -8176,6 +8199,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, "node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -8670,6 +8699,20 @@ "react": "^19.2.3" } }, + "node_modules/react-easy-crop": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.6.tgz", + "integrity": "sha512-Jw3/ozs8uXj3NpL511Suc4AHY+mLRO23rUgipXvNYKqezcFSYHxe4QXibBymkOoY6oOtLVMPO2HNPRHYvMPyTw==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", diff --git a/Frontend/Electron/package.json b/Frontend/Electron/package.json index 70863e0..ee91791 100644 --- a/Frontend/Electron/package.json +++ b/Frontend/Electron/package.json @@ -34,14 +34,21 @@ } ], "win": { - "target": ["nsis"] + "target": [ + "nsis" + ] }, "mac": { - "target": ["dmg", "zip"], + "target": [ + "dmg", + "zip" + ], "category": "public.app-category.social-networking" }, "linux": { - "target": ["AppImage"] + "target": [ + "AppImage" + ] }, "nsis": { "oneClick": true, @@ -49,6 +56,7 @@ } }, "dependencies": { + "@convex-dev/presence": "^0.3.0", "@livekit/components-react": "^2.9.17", "@livekit/components-styles": "^1.2.0", "convex": "^1.31.2", @@ -57,6 +65,7 @@ "livekit-client": "^2.16.1", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-easy-crop": "^5.5.6", "react-markdown": "^10.1.0", "react-router-dom": "^7.11.0", "react-syntax-highlighter": "^16.1.0", diff --git a/Frontend/Electron/preload.cjs b/Frontend/Electron/preload.cjs index ebf0750..26b804e 100644 --- a/Frontend/Electron/preload.cjs +++ b/Frontend/Electron/preload.cjs @@ -24,3 +24,14 @@ contextBridge.exposeInMainWorld('windowControls', { maximize: () => ipcRenderer.send('window-maximize'), close: () => ipcRenderer.send('window-close'), }); + +contextBridge.exposeInMainWorld('appSettings', { + get: (key) => ipcRenderer.invoke('get-setting', key), + set: (key, value) => ipcRenderer.invoke('set-setting', key, value), +}); + +contextBridge.exposeInMainWorld('sessionPersistence', { + save: (data) => ipcRenderer.invoke('save-session', data), + load: () => ipcRenderer.invoke('load-session'), + clear: () => ipcRenderer.invoke('clear-session'), +}); diff --git a/Frontend/Electron/src/App.jsx b/Frontend/Electron/src/App.jsx index b19bb10..2f21cf0 100644 --- a/Frontend/Electron/src/App.jsx +++ b/Frontend/Electron/src/App.jsx @@ -1,16 +1,100 @@ -import React from 'react'; -import { Routes, Route } from 'react-router-dom'; +import React, { useState, useEffect, useRef } from 'react'; +import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import Login from './pages/Login'; import Register from './pages/Register'; import Chat from './pages/Chat'; +const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; + +function AuthGuard({ children }) { + const [authState, setAuthState] = useState('loading'); // 'loading' | 'authenticated' | 'unauthenticated' + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + let cancelled = false; + + async function restoreSession() { + // Already have keys in sessionStorage — current session is active + if (sessionStorage.getItem('privateKey') && sessionStorage.getItem('signingKey')) { + if (!cancelled) setAuthState('authenticated'); + return; + } + + // Try restoring from safeStorage + if (window.sessionPersistence) { + try { + const session = await window.sessionPersistence.load(); + if (session && session.savedAt && (Date.now() - session.savedAt) < THIRTY_DAYS_MS) { + // Restore to localStorage + sessionStorage + localStorage.setItem('userId', session.userId); + localStorage.setItem('username', session.username); + if (session.publicKey) localStorage.setItem('publicKey', session.publicKey); + sessionStorage.setItem('signingKey', session.signingKey); + sessionStorage.setItem('privateKey', session.privateKey); + if (!cancelled) setAuthState('authenticated'); + return; + } + // Expired — clear stale session + if (session && session.savedAt) { + await window.sessionPersistence.clear(); + } + } catch (err) { + console.error('Session restore failed:', err); + } + } + + if (!cancelled) setAuthState('unauthenticated'); + } + + restoreSession(); + return () => { cancelled = true; }; + }, []); + + // Redirect once after auth state is determined (not on every route change) + const hasRedirected = useRef(false); + + useEffect(() => { + if (authState === 'loading' || hasRedirected.current) return; + hasRedirected.current = true; + + const isAuthPage = location.pathname === '/' || location.pathname === '/register'; + + if (authState === 'authenticated' && isAuthPage) { + navigate('/chat', { replace: true }); + } else if (authState === 'unauthenticated' && !isAuthPage) { + navigate('/', { replace: true }); + } + }, [authState]); + + if (authState === 'loading') { + return ( + <div style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + backgroundColor: 'var(--bg-primary, #313338)', + color: 'var(--text-normal, #dbdee1)', + fontSize: '16px', + }}> + Loading... + </div> + ); + } + + return children; +} + function App() { return ( - <Routes> - <Route path="/" element={<Login />} /> - <Route path="/register" element={<Register />} /> - <Route path="/chat" element={<Chat />} /> - </Routes> + <AuthGuard> + <Routes> + <Route path="/" element={<Login />} /> + <Route path="/register" element={<Register />} /> + <Route path="/chat" element={<Chat />} /> + </Routes> + </AuthGuard> ); } diff --git a/Frontend/Electron/src/assets/icons/crown.svg b/Frontend/Electron/src/assets/icons/crown.svg new file mode 100644 index 0000000..84ec03e --- /dev/null +++ b/Frontend/Electron/src/assets/icons/crown.svg @@ -0,0 +1 @@ +<svg class="ownerIcon__5d473 icon__5d473" aria-describedby="«r7fs»" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M5 18a1 1 0 0 0-1 1 3 3 0 0 0 3 3h10a3 3 0 0 0 3-3 1 1 0 0 0-1-1H5ZM3.04 7.76a1 1 0 0 0-1.52 1.15l2.25 6.42a1 1 0 0 0 .94.67h14.55a1 1 0 0 0 .95-.71l1.94-6.45a1 1 0 0 0-1.55-1.1l-4.11 3-3.55-5.33.82-.82a.83.83 0 0 0 0-1.18l-1.17-1.17a.83.83 0 0 0-1.18 0l-1.17 1.17a.83.83 0 0 0 0 1.18l.82.82-3.61 5.42-4.41-3.07Z" class=""></path></svg> \ No newline at end of file diff --git a/Frontend/Electron/src/assets/icons/invite_user.svg b/Frontend/Electron/src/assets/icons/invite_user.svg new file mode 100644 index 0000000..3bc48b6 --- /dev/null +++ b/Frontend/Electron/src/assets/icons/invite_user.svg @@ -0,0 +1 @@ +<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 3.08c.01.06.06.1.12.1ZM16.62 13.17c-.22.29-.65.37-.92.14-.34-.3-.7-.57-1.09-.82-.52-.33-.7-1.05-.47-1.63.11-.27.2-.57.26-.87.11-.54.55-1 1.1-.92 1.6.2 3.04.92 4.15 1.98.3.27-.25.95-.65.95a3 3 0 0 0-2.38 1.17ZM15.19 15.61c.13.16.02.39-.19.39a3 3 0 0 0-1.52 5.59c.2.12.26.41.02.41h-8a.5.5 0 0 1-.5-.5v-2.1c0-.25-.31-.33-.42-.1-.32.67-.67 1.58-.88 2.54a.2.2 0 0 1-.2.16A1.5 1.5 0 0 1 2 20.5a7.5 7.5 0 0 1 13.19-4.89ZM9.5 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM15.5 22Z" class=""></path><path fill="currentColor" d="M19 14a1 1 0 0 1 1 1v3h3a1 1 0 0 1 0 2h-3v3a1 1 0 0 1-2 0v-3h-3a1 1 0 1 1 0-2h3v-3a1 1 0 0 1 1-1Z" class=""></path></svg> \ No newline at end of file diff --git a/Frontend/Electron/src/components/AvatarCropModal.jsx b/Frontend/Electron/src/components/AvatarCropModal.jsx new file mode 100644 index 0000000..7b34195 --- /dev/null +++ b/Frontend/Electron/src/components/AvatarCropModal.jsx @@ -0,0 +1,134 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import Cropper from 'react-easy-crop'; + +function getCroppedImg(imageSrc, pixelCrop) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d'); + ctx.drawImage( + image, + pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height, + 0, 0, 256, 256 + ); + canvas.toBlob((blob) => { + if (!blob) return reject(new Error('Canvas toBlob failed')); + resolve(blob); + }, 'image/png'); + }; + image.onerror = reject; + image.src = imageSrc; + }); +} + +const AvatarCropModal = ({ imageUrl, onApply, onCancel }) => { + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + + const onCropComplete = useCallback((_croppedArea, croppedPixels) => { + setCroppedAreaPixels(croppedPixels); + }, []); + + const handleApply = useCallback(async () => { + if (!croppedAreaPixels) return; + const blob = await getCroppedImg(imageUrl, croppedAreaPixels); + onApply(blob); + }, [imageUrl, croppedAreaPixels, onApply]); + + useEffect(() => { + const handleKey = (e) => { + if (e.key === 'Escape') { + e.stopPropagation(); + onCancel(); + } + }; + window.addEventListener('keydown', handleKey, true); + return () => window.removeEventListener('keydown', handleKey, true); + }, [onCancel]); + + return ( + <div className="avatar-crop-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onCancel(); }}> + <div className="avatar-crop-dialog"> + {/* Header */} + <div className="avatar-crop-header"> + <h2 style={{ margin: 0, fontSize: '20px', fontWeight: 700, color: 'var(--header-primary)' }}> + Edit Image + </h2> + <button + onClick={onCancel} + style={{ + background: 'none', border: 'none', color: 'var(--header-secondary)', + fontSize: '24px', cursor: 'pointer', padding: '4px', lineHeight: 1, + }} + > + ✕ + </button> + </div> + + {/* Crop area */} + <div className="avatar-crop-area"> + <Cropper + image={imageUrl} + crop={crop} + zoom={zoom} + aspect={1} + cropShape="round" + showGrid={false} + onCropChange={setCrop} + onZoomChange={setZoom} + onCropComplete={onCropComplete} + /> + </div> + + {/* Zoom slider */} + <div className="avatar-crop-slider-row"> + <svg width="16" height="16" viewBox="0 0 24 24" fill="var(--header-secondary)"> + <path d="M15 3H9v2h6V3zm-4 18h2v-2h-2v2zm8-15.97l-1.41-1.41-1.76 1.76 1.41 1.41L19 5.03zM4.76 4.38L3.34 5.8 5.1 7.56 6.52 6.14 4.76 4.38zM21 11h-2v2h2v-2zM5 11H3v2h2v-2zm7-4a5 5 0 100 10 5 5 0 000-10z"/> + </svg> + <input + type="range" + min={1} + max={3} + step={0.01} + value={zoom} + onChange={(e) => setZoom(Number(e.target.value))} + className="avatar-crop-slider" + /> + <svg width="20" height="20" viewBox="0 0 24 24" fill="var(--header-secondary)"> + <path d="M15 3H9v2h6V3zm-4 18h2v-2h-2v2zm8-15.97l-1.41-1.41-1.76 1.76 1.41 1.41L19 5.03zM4.76 4.38L3.34 5.8 5.1 7.56 6.52 6.14 4.76 4.38zM21 11h-2v2h2v-2zM5 11H3v2h2v-2zm7-4a5 5 0 100 10 5 5 0 000-10z"/> + </svg> + </div> + + {/* Actions */} + <div className="avatar-crop-actions"> + <button + onClick={onCancel} + style={{ + background: 'none', border: 'none', color: 'var(--header-primary)', + cursor: 'pointer', fontSize: '14px', fontWeight: 500, padding: '8px 16px', + }} + > + Cancel + </button> + <button + onClick={handleApply} + style={{ + backgroundColor: 'var(--brand-experiment)', color: 'white', border: 'none', + borderRadius: '4px', padding: '8px 24px', cursor: 'pointer', + fontSize: '14px', fontWeight: 500, + }} + > + Apply + </button> + </div> + </div> + </div> + ); +}; + +export default AvatarCropModal; diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index d5071e4..8166c70 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -65,8 +65,36 @@ const extractUrls = (text) => { return text.match(urlRegex) || []; }; +const VIDEO_EXT_RE = /\.(mp4|webm|ogg|mov)(\?[^\s]*)?$/i; +const isVideoUrl = (url) => VIDEO_EXT_RE.test(url); + +const DirectVideo = ({ src, marginTop = 8 }) => { + const ref = useRef(null); + const [showControls, setShowControls] = useState(false); + const handlePlay = () => { + setShowControls(true); + if (ref.current) ref.current.play(); + }; + return ( + <div style={{ marginTop, position: 'relative', display: 'inline-block', maxWidth: '100%' }}> + <video + ref={ref} + src={src} + controls={showControls} + preload="metadata" + style={{ maxWidth: '100%', maxHeight: '300px', borderRadius: '8px', backgroundColor: 'black', display: 'block' }} + /> + {!showControls && ( + <div className="play-icon" onClick={handlePlay} style={{ cursor: 'pointer' }}> + ▶ + </div> + )} + </div> + ); +}; + const getYouTubeId = (link) => { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|shorts\/|watch\?v=|&v=)([^#&?]*).*/; const match = link.match(regExp); return (match && match[2].length === 11) ? match[2] : null; }; @@ -105,6 +133,16 @@ const isNewDay = (current, previous) => { || current.getFullYear() !== previous.getFullYear(); }; +const getProviderClass = (url) => { + try { + const hostname = new URL(url).hostname.replace(/^www\./, ''); + if (hostname === 'twitter.com' || hostname === 'x.com') return 'twitter-preview'; + if (hostname === 'open.spotify.com') return 'spotify-preview'; + if (hostname === 'reddit.com') return 'reddit-preview'; + } catch {} + return ''; +}; + const LinkPreview = ({ url }) => { const [metadata, setMetadata] = useState(metadataCache.get(url) || null); const [loading, setLoading] = useState(!metadataCache.has(url)); @@ -139,6 +177,11 @@ const LinkPreview = ({ url }) => { const videoId = getYouTubeId(url); const isYouTube = !!videoId; + const isDirectVideoUrl = isVideoUrl(url); + + if (isDirectVideoUrl) { + return <DirectVideo src={url} />; + } if (loading || !metadata || (!metadata.title && !metadata.image && !metadata.video)) return null; @@ -173,10 +216,14 @@ const LinkPreview = ({ url }) => { ); } + const providerClass = getProviderClass(url); + const isLargeImage = providerClass === 'twitter-preview' || metadata.type === 'article' || metadata.type === 'summary_large_image'; + return ( - <div className={`link-preview ${isYouTube ? 'youtube-preview' : ''}`} style={{ borderLeftColor: metadata.themeColor || '#202225' }}> + <div className={`link-preview ${isYouTube ? 'youtube-preview' : ''} ${providerClass} ${isLargeImage && !isYouTube ? 'large-image-layout' : ''}`} style={{ borderLeftColor: metadata.themeColor || '#202225' }}> <div className="preview-content"> {metadata.siteName && <div className="preview-site-name">{metadata.siteName}</div>} + {metadata.author && <div className="preview-author">{metadata.author}</div>} {metadata.title && ( <a href={url} onClick={(e) => { e.preventDefault(); window.cryptoAPI.openExternal(url); }} className="preview-title"> {metadata.title} @@ -194,11 +241,21 @@ const LinkPreview = ({ url }) => { /> </div> )} + {isLargeImage && !isYouTube && metadata.image && ( + <div className="preview-image-container large-image"> + <img src={metadata.image} alt="Preview" className="preview-image" /> + </div> + )} </div> - {metadata.image && (!isYouTube || !playing) && ( - <div className="preview-image-container" onClick={() => isYouTube && setPlaying(true)} style={isYouTube ? { cursor: 'pointer' } : {}}> + {!isLargeImage && !isYouTube && metadata.image && ( + <div className="preview-image-container"> <img src={metadata.image} alt="Preview" className="preview-image" /> - {isYouTube && <div className="play-icon">▶</div>} + </div> + )} + {isYouTube && metadata.image && !playing && ( + <div className="preview-image-container" onClick={() => setPlaying(true)} style={{ cursor: 'pointer' }}> + <img src={metadata.image} alt="Preview" className="preview-image" /> + <div className="play-icon">▶</div> </div> )} </div> @@ -1042,15 +1099,19 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u const urls = extractUrls(msg.content); const isOnlyUrl = urls.length === 1 && msg.content.trim() === urls[0]; const isGif = isOnlyUrl && (urls[0].includes('tenor.com') || urls[0].includes('giphy.com') || urls[0].endsWith('.gif')); + const isDirectVideo = isOnlyUrl && isVideoUrl(urls[0]); return ( <> - {!isGif && ( + {!isGif && !isDirectVideo && ( <ReactMarkdown remarkPlugins={[remarkGfm]} urlTransform={(url) => url} components={markdownComponents}> {formatEmojis(formatMentions(msg.content))} </ReactMarkdown> )} - {urls.map((url, i) => <LinkPreview key={i} url={url} />)} + {isDirectVideo && <DirectVideo src={urls[0]} marginTop={4} />} + {urls.filter(u => !(isDirectVideo && u === urls[0])).map((url, i) => ( + <LinkPreview key={i} url={url} /> + ))} </> ); }; @@ -1263,6 +1324,11 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u onBlur={saveSelection} onMouseUp={saveSelection} onKeyUp={saveSelection} + onPaste={(e) => { + e.preventDefault(); + const text = e.clipboardData.getData('text/plain'); + document.execCommand('insertText', false, text); + }} onInput={(e) => { const textContent = e.currentTarget.textContent; setInput(textContent); diff --git a/Frontend/Electron/src/components/DMList.jsx b/Frontend/Electron/src/components/DMList.jsx index 2be9204..6acad46 100644 --- a/Frontend/Electron/src/components/DMList.jsx +++ b/Frontend/Electron/src/components/DMList.jsx @@ -3,6 +3,7 @@ import { useConvex } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Tooltip from './Tooltip'; import Avatar from './Avatar'; +import { useOnlineUsers } from '../contexts/PresenceContext'; const STATUS_COLORS = { online: '#3ba55c', @@ -37,6 +38,7 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => { const [searchFocused, setSearchFocused] = useState(false); const searchRef = useRef(null); const searchInputRef = useRef(null); + const { resolveStatus } = useOnlineUsers(); const convex = useConvex(); @@ -200,7 +202,7 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => { <div style={{ flex: 1, overflowY: 'auto' }}> {(dmChannels || []).map(dm => { const isActive = activeDMChannel?.channel_id === dm.channel_id; - const status = dm.other_user_status || 'online'; + const effectiveStatus = resolveStatus(dm.other_user_status, dm.other_user_id); return ( <div key={dm.channel_id} @@ -213,7 +215,7 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => { <div style={{ position: 'absolute', bottom: -2, right: -2, width: 10, height: 10, borderRadius: '50%', - backgroundColor: STATUS_COLORS[status] || STATUS_COLORS.online, + backgroundColor: STATUS_COLORS[effectiveStatus] || STATUS_COLORS.offline, border: '2px solid var(--bg-secondary)' }} /> </div> @@ -222,7 +224,7 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => { {dm.other_username} </div> <div className="dm-item-status"> - {STATUS_LABELS[status] || 'Online'} + {STATUS_LABELS[effectiveStatus] || 'Offline'} </div> </div> </div> diff --git a/Frontend/Electron/src/components/FriendsView.jsx b/Frontend/Electron/src/components/FriendsView.jsx index 55c3c60..8d26b6f 100644 --- a/Frontend/Electron/src/components/FriendsView.jsx +++ b/Frontend/Electron/src/components/FriendsView.jsx @@ -2,12 +2,14 @@ import React, { useState } from 'react'; import { useQuery } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Avatar from './Avatar'; +import { useOnlineUsers } from '../contexts/PresenceContext'; const FriendsView = ({ onOpenDM }) => { const [activeTab, setActiveTab] = useState('Online'); const [addFriendSearch, setAddFriendSearch] = useState(''); const myId = localStorage.getItem('userId'); + const { resolveStatus } = useOnlineUsers(); const allUsers = useQuery(api.auth.getPublicKeys) || []; const users = allUsers.filter(u => u.id !== myId); @@ -31,7 +33,7 @@ const FriendsView = ({ onOpenDM }) => { }; const filteredUsers = activeTab === 'Online' - ? users.filter(u => u.status !== 'offline' && u.status !== 'invisible') + ? users.filter(u => resolveStatus(u.status, u.id) !== 'offline') : activeTab === 'Add Friend' ? users.filter(u => u.username?.toLowerCase().includes(addFriendSearch.toLowerCase())) : users; @@ -91,7 +93,9 @@ const FriendsView = ({ onOpenDM }) => { {/* Friends List */} <div style={{ flex: 1, overflowY: 'auto', padding: '0 20px' }}> - {filteredUsers.map(user => ( + {filteredUsers.map(user => { + const effectiveStatus = resolveStatus(user.status, user.id); + return ( <div key={user.id} className="friend-item" @@ -102,7 +106,7 @@ const FriendsView = ({ onOpenDM }) => { <div style={{ position: 'absolute', bottom: -2, right: -2, width: 10, height: 10, borderRadius: '50%', - backgroundColor: STATUS_COLORS[user.status] || STATUS_COLORS.online, + backgroundColor: STATUS_COLORS[effectiveStatus] || STATUS_COLORS.offline, border: '2px solid var(--bg-primary)' }} /> </div> @@ -111,7 +115,7 @@ const FriendsView = ({ onOpenDM }) => { {user.username ?? 'Unknown'} </div> <div style={{ color: 'var(--header-secondary)', fontSize: '12px' }}> - {user.status === 'dnd' ? 'Do Not Disturb' : (user.status || 'Online').charAt(0).toUpperCase() + (user.status || 'online').slice(1)} + {effectiveStatus === 'dnd' ? 'Do Not Disturb' : effectiveStatus.charAt(0).toUpperCase() + effectiveStatus.slice(1)} </div> </div> </div> @@ -134,7 +138,8 @@ const FriendsView = ({ onOpenDM }) => { </div> </div> </div> - ))} + ); + })} </div> </div> ); diff --git a/Frontend/Electron/src/components/MembersList.jsx b/Frontend/Electron/src/components/MembersList.jsx index 1f37cbc..cc6da7c 100644 --- a/Frontend/Electron/src/components/MembersList.jsx +++ b/Frontend/Electron/src/components/MembersList.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { useQuery } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; +import { useOnlineUsers } from '../contexts/PresenceContext'; const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245']; @@ -25,11 +26,12 @@ const MembersList = ({ channelId, visible, onMemberClick }) => { api.members.getChannelMembers, channelId ? { channelId } : "skip" ) || []; + const { resolveStatus } = useOnlineUsers(); if (!visible) return null; - const onlineMembers = members.filter(m => m.status !== 'offline' && m.status !== 'invisible'); - const offlineMembers = members.filter(m => m.status === 'offline' || m.status === 'invisible'); + const onlineMembers = members.filter(m => resolveStatus(m.status, m.id) !== 'offline'); + const offlineMembers = members.filter(m => resolveStatus(m.status, m.id) === 'offline'); // Group online members by highest hoisted role const roleGroups = {}; @@ -54,13 +56,14 @@ const MembersList = ({ channelId, visible, onMemberClick }) => { const renderMember = (member) => { const topRole = member.roles.length > 0 ? member.roles[0] : null; const nameColor = topRole && topRole.name !== '@everyone' ? topRole.color : '#fff'; + const effectiveStatus = resolveStatus(member.status, member.id); return ( <div key={member.id} className="member-item" onClick={() => onMemberClick && onMemberClick(member)} - style={member.status === 'offline' || member.status === 'invisible' ? { opacity: 0.3 } : {}} + style={effectiveStatus === 'offline' ? { opacity: 0.3 } : {}} > <div className="member-avatar-wrapper"> {member.avatarUrl ? ( @@ -80,7 +83,7 @@ const MembersList = ({ channelId, visible, onMemberClick }) => { )} <div className="member-status-dot" - style={{ backgroundColor: STATUS_COLORS[member.status] || STATUS_COLORS.online }} + style={{ backgroundColor: STATUS_COLORS[effectiveStatus] || STATUS_COLORS.offline }} /> </div> <div className="member-info"> diff --git a/Frontend/Electron/src/components/Sidebar.jsx b/Frontend/Electron/src/components/Sidebar.jsx index adc9d82..844d0ed 100644 --- a/Frontend/Electron/src/components/Sidebar.jsx +++ b/Frontend/Electron/src/components/Sidebar.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; -import { useConvex, useMutation } from 'convex/react'; +import { useNavigate } from 'react-router-dom'; +import { useConvex, useMutation, useQuery } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Tooltip from './Tooltip'; import { useVoice } from '../contexts/VoiceContext'; @@ -8,7 +9,7 @@ import ServerSettingsModal from './ServerSettingsModal'; import ScreenShareModal from './ScreenShareModal'; import DMList from './DMList'; import Avatar from './Avatar'; -import ThemeSelector from './ThemeSelector'; +import UserSettings from './UserSettings'; import { Track } from 'livekit-client'; import muteIcon from '../assets/icons/mute.svg'; import mutedIcon from '../assets/icons/muted.svg'; @@ -19,6 +20,7 @@ import voiceIcon from '../assets/icons/voice.svg'; import disconnectIcon from '../assets/icons/disconnect.svg'; import cameraIcon from '../assets/icons/camera.svg'; import screenIcon from '../assets/icons/screen.svg'; +import inviteUserIcon from '../assets/icons/invite_user.svg'; const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245']; @@ -101,11 +103,46 @@ const STATUS_OPTIONS = [ ]; const UserControlPanel = ({ username, userId }) => { - const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState } = useVoice(); + const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState, disconnectVoice } = useVoice(); const [showStatusMenu, setShowStatusMenu] = useState(false); - const [showThemeSelector, setShowThemeSelector] = useState(false); + const [showUserSettings, setShowUserSettings] = useState(false); const [currentStatus, setCurrentStatus] = useState('online'); const updateStatusMutation = useMutation(api.auth.updateStatus); + const navigate = useNavigate(); + + // Fetch stored status preference from server and sync local state + const allUsers = useQuery(api.auth.getPublicKeys) || []; + const myUser = allUsers.find(u => u.id === userId); + React.useEffect(() => { + if (myUser) { + if (myUser.status && myUser.status !== 'offline') { + setCurrentStatus(myUser.status); + } else if (!myUser.status || myUser.status === 'offline') { + // First login or no preference set yet — default to "online" + setCurrentStatus('online'); + if (userId) { + updateStatusMutation({ userId, status: 'online' }).catch(() => {}); + } + } + } + }, [myUser?.status]); + + const handleLogout = async () => { + // Disconnect voice if connected + if (connectionState === 'connected') { + try { disconnectVoice(); } catch {} + } + // Clear persisted session + if (window.sessionPersistence) { + try { await window.sessionPersistence.clear(); } catch {} + } + // Clear storage (preserve theme) + const theme = localStorage.getItem('theme'); + localStorage.clear(); + if (theme) localStorage.setItem('theme', theme); + sessionStorage.clear(); + navigate('/'); + }; const effectiveMute = isMuted || isDeafened; const statusColor = STATUS_OPTIONS.find(s => s.value === currentStatus)?.color || '#3ba55c'; @@ -191,15 +228,31 @@ const UserControlPanel = ({ username, userId }) => { </button> </Tooltip> <Tooltip text="User Settings" position="top"> - <button style={controlButtonStyle} onClick={() => setShowThemeSelector(true)}> + <button style={controlButtonStyle} onClick={() => setShowUserSettings(true)}> <ColoredIcon src={settingsIcon} color={ICON_COLOR_DEFAULT} /> </button> </Tooltip> + <Tooltip text="Log Out" position="top"> + <button style={controlButtonStyle} onClick={handleLogout}> + <svg width="20" height="20" viewBox="0 0 24 24" fill="none"> + <path d="M16 17L21 12L16 7" stroke={ICON_COLOR_DEFAULT} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> + <path d="M21 12H9" stroke={ICON_COLOR_DEFAULT} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> + <path d="M9 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H9" stroke={ICON_COLOR_DEFAULT} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> + </svg> + </button> + </Tooltip> </div> - {showThemeSelector && <ThemeSelector onClose={() => setShowThemeSelector(false)} />} + {showUserSettings && ( + <UserSettings + onClose={() => setShowUserSettings(false)} + userId={userId} + username={username} + onLogout={handleLogout} + /> + )} </div> ); }; @@ -445,7 +498,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe }; const renderDMView = () => ( - <div className="channel-list" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}> + <div className="channel-list" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--bg-tertiary)', borderLeft: '1px solid var(--app-frame-border)', borderTop: '1px solid var(--app-frame-border)', borderRadius: '8px 0 0 0' }}> <DMList dmChannels={dmChannels} activeDMChannel={activeDMChannel} @@ -504,13 +557,15 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe }, [channels]); const renderServerView = () => ( - <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}> - <div className="server-header" onClick={() => setIsServerSettingsOpen(true)}> - <span>Secure Chat</span> - <span className="server-header-chevron">▾</span> + <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--bg-tertiary)', borderLeft: '1px solid var(--app-frame-border)', borderTop: '1px solid var(--app-frame-border)', borderRadius: '8px 0 0 0' }}> + <div className="server-header" style={{ borderBottom: '1px solid var(--app-frame-border)' }}> + <span className="server-header-name" onClick={() => setIsServerSettingsOpen(true)}>Secure Chat</span> + <button className="server-header-invite" onClick={handleCreateInvite} title="Invite People"> + <img src={inviteUserIcon} alt="Invite" /> + </button> </div> - <div className="channel-list" style={{ flex: 1, overflowY: 'auto' }}> + <div className="channel-list" style={{ flex: 1, overflowY: 'auto', backgroundColor: 'var(--bg-tertiary)' }}> {isCreating && ( <div style={{ padding: '0 8px', marginBottom: '4px' }}> <form onSubmit={handleSubmitCreate}> diff --git a/Frontend/Electron/src/components/UserSettings.jsx b/Frontend/Electron/src/components/UserSettings.jsx new file mode 100644 index 0000000..5f0e090 --- /dev/null +++ b/Frontend/Electron/src/components/UserSettings.jsx @@ -0,0 +1,712 @@ +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'; + +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: 'appearance', label: 'Appearance', section: 'USER SETTINGS' }, + { id: 'voice', label: 'Voice & Video', section: 'APP SETTINGS' }, + { id: 'keybinds', label: 'Keybinds', 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(<div key={`sep-${i}`} style={{ height: '1px', backgroundColor: 'var(--border-subtle)', margin: '8px 10px' }} />); + } + items.push( + <div key={`hdr-${tab.section}`} style={{ + fontSize: '12px', fontWeight: '700', color: 'var(--text-muted)', + marginBottom: '6px', textTransform: 'uppercase', padding: '0 10px' + }}> + {tab.section} + </div> + ); + lastSection = tab.section; + } + items.push( + <div + key={tab.id} + onClick={() => 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} + </div> + ); + }); + + items.push(<div key="sep-logout" style={{ height: '1px', backgroundColor: 'var(--border-subtle)', margin: '8px 10px' }} />); + items.push( + <div + key="logout" + onClick={onLogout} + style={{ + padding: '6px 10px', borderRadius: '4px', cursor: 'pointer', fontSize: '15px', + color: '#ed4245', display: 'flex', justifyContent: 'space-between', alignItems: 'center', + }} + > + Log Out + <svg width="16" height="16" viewBox="0 0 24 24" fill="none"> + <path d="M16 17L21 12L16 7" stroke="#ed4245" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> + <path d="M21 12H9" stroke="#ed4245" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> + <path d="M9 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H9" stroke="#ed4245" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> + </svg> + </div> + ); + + return items; + }; + + return ( + <div style={{ + position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, + backgroundColor: 'var(--bg-primary)', zIndex: 1000, + display: 'flex', color: 'var(--text-normal)', + }}> + {/* Sidebar */} + <div style={{ + width: '218px', backgroundColor: 'var(--bg-secondary)', + display: 'flex', flexDirection: 'column', alignItems: 'flex-end', + padding: '60px 6px 60px 20px', overflowY: 'auto', + }}> + <div style={{ width: '100%' }}> + {renderSidebar()} + </div> + </div> + + {/* Content */} + <div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start', overflowY: 'auto' }}> + <div style={{ flex: 1, maxWidth: '740px', padding: '60px 40px 80px', position: 'relative' }}> + {activeTab === 'account' && <MyAccountTab userId={userId} username={username} />} + {activeTab === 'appearance' && <AppearanceTab />} + {activeTab === 'voice' && <VoiceVideoTab />} + {activeTab === 'keybinds' && <KeybindsTab />} + </div> + + {/* Right spacer with close button */} + <div style={{ flex: '0 0 36px', paddingTop: '60px', marginLeft: '8px' }}> + <button + onClick={onClose} + style={{ + width: '36px', height: '36px', borderRadius: '50%', + border: '2px solid var(--header-secondary)', background: 'transparent', + color: 'var(--header-secondary)', cursor: 'pointer', + display: 'flex', alignItems: 'center', justifyContent: 'center', + fontSize: '18px', + }} + > + ✕ + </button> + <div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--header-secondary)', textAlign: 'center', marginTop: '4px' }}> + ESC + </div> + </div> + <div style={{ flex: '0.5' }} /> + </div> + </div> + ); +}; + +/* ========================================= + 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); + + 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; + setHasChanges(changed); + }, [displayName, aboutMe, customStatus, avatarFile, 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 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; + } + const args = { userId, displayName, aboutMe, customStatus }; + if (avatarStorageId) args.avatarStorageId = avatarStorageId; + await convex.mutation(api.auth.updateProfile, args); + setAvatarFile(null); + 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); + if (avatarPreview) { + URL.revokeObjectURL(avatarPreview); + setAvatarPreview(null); + } + if (rawImageUrl) { + URL.revokeObjectURL(rawImageUrl); + setRawImageUrl(null); + } + setShowCropModal(false); + }; + + const avatarUrl = avatarPreview || currentUser?.avatarUrl; + + return ( + <div> + <h2 style={{ color: 'var(--header-primary)', margin: '0 0 20px', fontSize: '20px' }}>My Account</h2> + + {/* Profile card */} + <div style={{ backgroundColor: 'var(--bg-secondary)', borderRadius: '8px', overflow: 'hidden' }}> + {/* Banner */} + <div style={{ height: '100px', backgroundColor: 'var(--brand-experiment)' }} /> + + {/* Profile body */} + <div style={{ padding: '0 16px 16px', position: 'relative' }}> + {/* Avatar */} + <div + className="user-settings-avatar-wrapper" + onClick={() => fileInputRef.current?.click()} + style={{ marginTop: '-40px', marginBottom: '12px', width: 'fit-content', cursor: 'pointer', position: 'relative' }} + > + <Avatar username={username} avatarUrl={avatarUrl} size={80} /> + <div className="user-settings-avatar-overlay"> + CHANGE<br/>AVATAR + </div> + <input + ref={fileInputRef} + type="file" + accept="image/*" + onChange={handleAvatarChange} + style={{ display: 'none' }} + /> + </div> + + {/* Fields */} + <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> + {/* Username (read-only) */} + <div> + <label style={{ + display: 'block', color: 'var(--header-secondary)', fontSize: '12px', + fontWeight: '700', textTransform: 'uppercase', marginBottom: '8px', + }}> + Username + </label> + <div style={{ + backgroundColor: 'var(--bg-tertiary)', borderRadius: '4px', padding: '10px', + color: 'var(--text-muted)', fontSize: '16px', + }}> + {username} + </div> + </div> + + {/* Display Name */} + <div> + <label style={{ + display: 'block', color: 'var(--header-secondary)', fontSize: '12px', + fontWeight: '700', textTransform: 'uppercase', marginBottom: '8px', + }}> + Display Name + </label> + <input + type="text" + value={displayName} + onChange={(e) => 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', + }} + /> + </div> + + {/* About Me */} + <div> + <label style={{ + display: 'block', color: 'var(--header-secondary)', fontSize: '12px', + fontWeight: '700', textTransform: 'uppercase', marginBottom: '8px', + }}> + About Me + </label> + <textarea + value={aboutMe} + onChange={(e) => setAboutMe(e.target.value.slice(0, 190))} + placeholder="Tell others about yourself" + rows={3} + style={{ + width: '100%', backgroundColor: 'var(--bg-tertiary)', border: 'none', + borderRadius: '4px', padding: '10px', color: 'var(--text-normal)', + fontSize: '16px', outline: 'none', resize: 'none', fontFamily: 'inherit', + boxSizing: 'border-box', + }} + /> + <div style={{ fontSize: '12px', color: 'var(--text-muted)', textAlign: 'right' }}> + {aboutMe.length}/190 + </div> + </div> + + {/* Custom Status */} + <div> + <label style={{ + display: 'block', color: 'var(--header-secondary)', fontSize: '12px', + fontWeight: '700', textTransform: 'uppercase', marginBottom: '8px', + }}> + Custom Status + </label> + <input + type="text" + value={customStatus} + onChange={(e) => setCustomStatus(e.target.value)} + placeholder="Set a custom status" + style={{ + width: '100%', backgroundColor: 'var(--bg-tertiary)', border: 'none', + borderRadius: '4px', padding: '10px', color: 'var(--text-normal)', + fontSize: '16px', outline: 'none', boxSizing: 'border-box', + }} + /> + </div> + </div> + </div> + </div> + + {/* Save bar */} + {hasChanges && ( + <div style={{ + position: 'sticky', bottom: '0', left: 0, right: 0, + backgroundColor: 'var(--bg-tertiary)', borderRadius: '4px', + padding: '10px 16px', marginTop: '16px', + display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '12px', + boxShadow: '0 -2px 10px rgba(0,0,0,0.2)', + }}> + <span style={{ color: 'var(--text-muted)', fontSize: '14px', marginRight: 'auto' }}> + Careful — you have unsaved changes! + </span> + <button + onClick={handleReset} + style={{ + background: 'transparent', border: 'none', color: 'var(--header-primary)', + cursor: 'pointer', fontSize: '14px', fontWeight: '500', padding: '8px 16px', + }} + > + Reset + </button> + <button + onClick={handleSave} + disabled={saving} + style={{ + backgroundColor: '#3ba55c', color: 'white', border: 'none', + borderRadius: '4px', padding: '8px 24px', cursor: saving ? 'not-allowed' : 'pointer', + fontSize: '14px', fontWeight: '500', opacity: saving ? 0.7 : 1, + }} + > + {saving ? 'Saving...' : 'Save Changes'} + </button> + </div> + )} + + {showCropModal && rawImageUrl && ( + <AvatarCropModal + imageUrl={rawImageUrl} + onApply={handleCropApply} + onCancel={handleCropCancel} + /> + )} + </div> + ); +}; + +/* ========================================= + APPEARANCE TAB + ========================================= */ +const AppearanceTab = () => { + const { theme, setTheme } = useTheme(); + + return ( + <div> + <h2 style={{ color: 'var(--header-primary)', margin: '0 0 20px', fontSize: '20px' }}>Appearance</h2> + <div style={{ marginBottom: '16px' }}> + <label style={{ + display: 'block', color: 'var(--header-secondary)', fontSize: '12px', + fontWeight: '700', textTransform: 'uppercase', marginBottom: '12px', + }}> + Theme + </label> + <div className="theme-selector-grid"> + {Object.values(THEMES).map((themeKey) => { + const preview = THEME_PREVIEWS[themeKey]; + const isActive = theme === themeKey; + return ( + <div + key={themeKey} + className={`theme-card ${isActive ? 'active' : ''}`} + onClick={() => setTheme(themeKey)} + > + <div className="theme-preview" style={{ backgroundColor: preview.bg }}> + <div className="theme-preview-sidebar" style={{ backgroundColor: preview.sidebar }}> + <div className="theme-preview-channel" style={{ backgroundColor: preview.tertiary }} /> + <div className="theme-preview-channel" style={{ backgroundColor: preview.tertiary, width: '60%' }} /> + </div> + <div className="theme-preview-chat"> + <div className="theme-preview-message" style={{ backgroundColor: preview.sidebar, opacity: 0.6 }} /> + <div className="theme-preview-message" style={{ backgroundColor: preview.sidebar, opacity: 0.4, width: '70%' }} /> + </div> + </div> + <div className="theme-card-label"> + <div className={`theme-radio ${isActive ? 'active' : ''}`}> + {isActive && <div className="theme-radio-dot" />} + </div> + <span>{THEME_LABELS[themeKey]}</span> + </div> + </div> + ); + })} + </div> + </div> + </div> + ); +}; + +/* ========================================= + VOICE & VIDEO TAB + ========================================= */ +const VoiceVideoTab = () => { + const [inputDevices, setInputDevices] = useState([]); + const [outputDevices, setOutputDevices] = useState([]); + const [selectedInput, setSelectedInput] = useState(() => localStorage.getItem('voiceInputDevice') || 'default'); + const [selectedOutput, setSelectedOutput] = useState(() => localStorage.getItem('voiceOutputDevice') || 'default'); + const [inputVolume, setInputVolume] = useState(() => parseInt(localStorage.getItem('voiceInputVolume') || '100')); + const [outputVolume, setOutputVolume] = useState(() => parseInt(localStorage.getItem('voiceOutputVolume') || '100')); + const [micTesting, setMicTesting] = useState(false); + const [micLevel, setMicLevel] = useState(0); + const micStreamRef = useRef(null); + const animFrameRef = useRef(null); + const analyserRef = useRef(null); + + useEffect(() => { + const enumerate = async () => { + try { + // Request permission to get labels + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + stream.getTracks().forEach(t => t.stop()); + + const devices = await navigator.mediaDevices.enumerateDevices(); + setInputDevices(devices.filter(d => d.kind === 'audioinput')); + setOutputDevices(devices.filter(d => d.kind === 'audiooutput')); + } catch (err) { + console.error('Failed to enumerate devices:', err); + } + }; + enumerate(); + }, []); + + useEffect(() => { + localStorage.setItem('voiceInputDevice', selectedInput); + }, [selectedInput]); + + useEffect(() => { + localStorage.setItem('voiceOutputDevice', selectedOutput); + }, [selectedOutput]); + + useEffect(() => { + localStorage.setItem('voiceInputVolume', String(inputVolume)); + }, [inputVolume]); + + useEffect(() => { + localStorage.setItem('voiceOutputVolume', String(outputVolume)); + }, [outputVolume]); + + const startMicTest = async () => { + try { + const constraints = { audio: selectedInput !== 'default' ? { deviceId: { exact: selectedInput } } : true }; + const stream = await navigator.mediaDevices.getUserMedia(constraints); + micStreamRef.current = stream; + + const audioCtx = new AudioContext(); + const source = audioCtx.createMediaStreamSource(stream); + const analyser = audioCtx.createAnalyser(); + analyser.fftSize = 256; + source.connect(analyser); + analyserRef.current = analyser; + + setMicTesting(true); + + const dataArray = new Uint8Array(analyser.frequencyBinCount); + const tick = () => { + analyser.getByteFrequencyData(dataArray); + const avg = dataArray.reduce((sum, v) => sum + v, 0) / dataArray.length; + setMicLevel(Math.min(100, (avg / 128) * 100)); + animFrameRef.current = requestAnimationFrame(tick); + }; + tick(); + } catch (err) { + console.error('Mic test failed:', err); + } + }; + + const stopMicTest = useCallback(() => { + if (micStreamRef.current) { + micStreamRef.current.getTracks().forEach(t => t.stop()); + micStreamRef.current = null; + } + if (animFrameRef.current) { + cancelAnimationFrame(animFrameRef.current); + animFrameRef.current = null; + } + setMicTesting(false); + setMicLevel(0); + }, []); + + useEffect(() => { + return () => stopMicTest(); + }, [stopMicTest]); + + const selectStyle = { + width: '100%', backgroundColor: 'var(--bg-tertiary)', border: 'none', + borderRadius: '4px', padding: '10px', color: 'var(--text-normal)', + fontSize: '14px', outline: 'none', boxSizing: 'border-box', + }; + + const labelStyle = { + display: 'block', color: 'var(--header-secondary)', fontSize: '12px', + fontWeight: '700', textTransform: 'uppercase', marginBottom: '8px', + }; + + return ( + <div> + <h2 style={{ color: 'var(--header-primary)', margin: '0 0 20px', fontSize: '20px' }}>Voice & Video</h2> + + <div style={{ display: 'flex', gap: '16px', marginBottom: '24px' }}> + {/* Input Device */} + <div style={{ flex: 1 }}> + <label style={labelStyle}>Input Device</label> + <select + value={selectedInput} + onChange={(e) => setSelectedInput(e.target.value)} + style={selectStyle} + > + <option value="default">Default</option> + {inputDevices.map(d => ( + <option key={d.deviceId} value={d.deviceId}> + {d.label || `Microphone ${d.deviceId.slice(0, 8)}`} + </option> + ))} + </select> + </div> + + {/* Output Device */} + <div style={{ flex: 1 }}> + <label style={labelStyle}>Output Device</label> + <select + value={selectedOutput} + onChange={(e) => setSelectedOutput(e.target.value)} + style={selectStyle} + > + <option value="default">Default</option> + {outputDevices.map(d => ( + <option key={d.deviceId} value={d.deviceId}> + {d.label || `Speaker ${d.deviceId.slice(0, 8)}`} + </option> + ))} + </select> + </div> + </div> + + {/* Input Volume */} + <div style={{ marginBottom: '20px' }}> + <label style={labelStyle}>Input Volume</label> + <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> + <input + type="range" + min="0" + max="100" + value={inputVolume} + onChange={(e) => setInputVolume(parseInt(e.target.value))} + className="voice-slider" + /> + <span style={{ color: 'var(--text-normal)', fontSize: '14px', minWidth: '36px' }}>{inputVolume}%</span> + </div> + </div> + + {/* Output Volume */} + <div style={{ marginBottom: '24px' }}> + <label style={labelStyle}>Output Volume</label> + <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> + <input + type="range" + min="0" + max="100" + value={outputVolume} + onChange={(e) => setOutputVolume(parseInt(e.target.value))} + className="voice-slider" + /> + <span style={{ color: 'var(--text-normal)', fontSize: '14px', minWidth: '36px' }}>{outputVolume}%</span> + </div> + </div> + + {/* Mic Test */} + <div style={{ marginBottom: '24px' }}> + <label style={labelStyle}>Mic Test</label> + <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> + <button + onClick={micTesting ? stopMicTest : startMicTest} + style={{ + backgroundColor: micTesting ? '#ed4245' : 'var(--brand-experiment)', + color: 'white', border: 'none', borderRadius: '4px', + padding: '8px 16px', cursor: 'pointer', fontSize: '14px', fontWeight: '500', + flexShrink: 0, + }} + > + {micTesting ? 'Stop Testing' : 'Let\'s Check'} + </button> + <div className="mic-level-bar"> + <div className="mic-level-fill" style={{ width: `${micLevel}%` }} /> + </div> + </div> + </div> + </div> + ); +}; + +/* ========================================= + KEYBINDS TAB + ========================================= */ +const KeybindsTab = () => { + const keybinds = [ + { action: 'Quick Switcher', keys: 'Ctrl+K' }, + { action: 'Toggle Mute', keys: 'Ctrl+Shift+M' }, + ]; + + return ( + <div> + <h2 style={{ color: 'var(--header-primary)', margin: '0 0 20px', fontSize: '20px' }}>Keybinds</h2> + + <div style={{ + backgroundColor: 'var(--bg-secondary)', borderRadius: '8px', padding: '16px', + marginBottom: '16px', + }}> + <p style={{ color: 'var(--text-muted)', fontSize: '14px', margin: '0 0 16px' }}> + Keybind configuration coming soon. Current keybinds are shown below. + </p> + + <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> + {keybinds.map(kb => ( + <div key={kb.action} style={{ + display: 'flex', alignItems: 'center', justifyContent: 'space-between', + padding: '10px 12px', backgroundColor: 'var(--bg-tertiary)', borderRadius: '4px', + }}> + <span style={{ color: 'var(--text-normal)', fontSize: '14px' }}>{kb.action}</span> + <kbd style={{ + backgroundColor: 'var(--bg-primary)', padding: '4px 8px', borderRadius: '4px', + fontSize: '13px', color: 'var(--header-primary)', fontFamily: 'inherit', + border: '1px solid var(--border-subtle)', + }}> + {kb.keys} + </kbd> + </div> + ))} + </div> + </div> + </div> + ); +}; + +export default UserSettings; diff --git a/Frontend/Electron/src/contexts/PresenceContext.jsx b/Frontend/Electron/src/contexts/PresenceContext.jsx new file mode 100644 index 0000000..486fe14 --- /dev/null +++ b/Frontend/Electron/src/contexts/PresenceContext.jsx @@ -0,0 +1,47 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import usePresence from '@convex-dev/presence/react'; +import { api } from '../../../../convex/_generated/api'; + +const PresenceContext = createContext({ + onlineUsers: new Set(), + resolveStatus: (storedStatus, userId) => storedStatus || 'offline', +}); + +export const useOnlineUsers = () => useContext(PresenceContext); + +/** + * Status resolution logic: + * - If user is NOT connected (no heartbeat) → "offline" + * - If user IS connected and chose "invisible" → "offline" + * - If user IS connected → show their chosen status (online/idle/dnd) + */ +function resolveStatusFn(onlineUsers, storedStatus, userId) { + if (!onlineUsers.has(userId)) return 'offline'; + if (storedStatus === 'invisible') return 'offline'; + return storedStatus || 'online'; +} + +export const PresenceProvider = ({ userId, children }) => { + const presenceState = usePresence(api.presence, 'global', userId); + + const onlineUsers = useMemo(() => { + const set = new Set(); + if (presenceState) { + for (const p of presenceState) { + if (p.online) set.add(p.userId); + } + } + return set; + }, [presenceState]); + + const value = useMemo(() => ({ + onlineUsers, + resolveStatus: (storedStatus, uid) => resolveStatusFn(onlineUsers, storedStatus, uid), + }), [onlineUsers]); + + return ( + <PresenceContext.Provider value={value}> + {children} + </PresenceContext.Provider> + ); +}; diff --git a/Frontend/Electron/src/contexts/ThemeContext.jsx b/Frontend/Electron/src/contexts/ThemeContext.jsx index 861c570..5eca109 100644 --- a/Frontend/Electron/src/contexts/ThemeContext.jsx +++ b/Frontend/Electron/src/contexts/ThemeContext.jsx @@ -23,9 +23,21 @@ export function ThemeProvider({ children }) { return localStorage.getItem(STORAGE_KEY) || THEMES.DARK; }); + // On mount, check settings.json as fallback when localStorage is empty + useEffect(() => { + if (!localStorage.getItem(STORAGE_KEY) && window.appSettings) { + window.appSettings.get('theme').then((saved) => { + if (saved && Object.values(THEMES).includes(saved)) { + setTheme(saved); + } + }); + } + }, []); + useEffect(() => { document.documentElement.className = theme; localStorage.setItem(STORAGE_KEY, theme); + window.appSettings?.set('theme', theme); }, [theme]); return ( diff --git a/Frontend/Electron/src/index.css b/Frontend/Electron/src/index.css index 30c79cf..4fdbc7a 100644 --- a/Frontend/Electron/src/index.css +++ b/Frontend/Electron/src/index.css @@ -135,7 +135,7 @@ body { .sidebar { width: 312px; min-width: 312px; - background-color: var(--bg-secondary); + background-color: var(--bg-tertiary); display: flex; flex-direction: row; flex-shrink: 0; @@ -152,6 +152,11 @@ body { flex-shrink: 0; } +.ownerIcon { + color: var(--text-feedback-warning); + margin-inline-start: 4px; +} + .server-icon { width: 48px; height: 48px; @@ -242,6 +247,7 @@ body { .messages-list { flex: 1; + overflow-x: hidden; overflow-y: auto; padding: 0 0 20px 0; display: flex; @@ -250,11 +256,11 @@ body { .messages-list::-webkit-scrollbar { width: 8px; - background-color: var(--bg-secondary); + background-color: var(--bg-primary); } .messages-list::-webkit-scrollbar-thumb { - background-color: var(--bg-tertiary); + background-color: #666770; border-radius: 4px; } @@ -574,6 +580,50 @@ body { font-size: 20px; } +/* Preview author line */ +.preview-author { + font-size: 13px; + color: var(--header-primary); + font-weight: 500; + margin-bottom: 2px; +} + +/* Provider-branded previews */ +.twitter-preview { + border-left-color: #1da1f2 !important; +} + +.twitter-preview .preview-description { + -webkit-line-clamp: 6; + line-clamp: 6; +} + +.spotify-preview { + border-left-color: #1db954 !important; +} + +.reddit-preview { + border-left-color: #ff4500 !important; +} + +/* Large image layout: image below content at full width */ +.large-image-layout { + flex-direction: column; + gap: 8px; +} + +.large-image-layout .preview-image-container.large-image { + width: 100%; + max-width: 400px; +} + +.large-image-layout .preview-image-container.large-image .preview-image { + width: 100%; + max-width: 100%; + max-height: 300px; + object-fit: cover; +} + .youtube-preview { flex-direction: column; align-items: flex-start; @@ -722,7 +772,6 @@ body { -webkit-app-region: drag; z-index: 10000; flex-shrink: 0; - border-bottom: 1px solid var(--bg-tertiary); } .titlebar-drag-region { @@ -864,7 +913,7 @@ body { .members-list { width: 240px; min-width: 240px; - background-color: var(--bg-secondary); + background-color: var(--bg-primary); border-left: 1px solid var(--border-subtle); overflow-y: auto; padding: 16px 8px; @@ -1894,25 +1943,58 @@ body { height: 48px; display: flex; align-items: center; - justify-content: space-between; padding: 0 16px; border-bottom: 1px solid var(--bg-tertiary); - cursor: pointer; flex-shrink: 0; font-weight: 600; font-size: 15px; color: var(--header-primary); + gap: 8px; +} + +.server-header-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + border-radius: 4px; + padding: 4px 4px; transition: background-color 0.1s; } -.server-header:hover { +.server-header-name:hover { background-color: var(--background-modifier-hover); } -.server-header-chevron { - font-size: 10px; - color: var(--text-muted); - transition: transform 0.2s; +.server-header-invite { + flex-shrink: 0; + background: none; + border: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--interactive-normal); + transition: color 0.15s, background-color 0.15s; +} + +.server-header-invite:hover { + color: var(--interactive-hover); + background-color: var(--background-modifier-hover); +} + +.server-header-invite img { + width: 20px; + height: 20px; + filter: brightness(0) invert(0.7); +} + +.server-header-invite:hover img { + filter: brightness(0) invert(0.9); } /* ============================================ @@ -2280,4 +2362,173 @@ body { height: 10px; border-radius: 50%; background-color: var(--control-primary-background-default); +} + +/* ============================================ + USER SETTINGS - AVATAR OVERLAY + ============================================ */ +.user-settings-avatar-wrapper { + position: relative; + border-radius: 50%; + overflow: hidden; +} + +.user-settings-avatar-overlay { + position: absolute; + top: 0; + left: 0; + width: 80px; + height: 80px; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + text-align: center; + line-height: 1.3; + opacity: 0; + transition: opacity 0.15s; + pointer-events: none; +} + +.user-settings-avatar-wrapper:hover .user-settings-avatar-overlay { + opacity: 1; +} + +/* ============================================ + AVATAR CROP MODAL + ============================================ */ +.avatar-crop-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10001; +} + +.avatar-crop-dialog { + width: 440px; + background-color: var(--bg-tertiary); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + overflow: hidden; +} + +.avatar-crop-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; +} + +.avatar-crop-area { + position: relative; + height: 300px; + background-color: #000; +} + +.avatar-crop-slider-row { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; +} + +.avatar-crop-slider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 6px; + background: var(--bg-secondary); + border-radius: 3px; + outline: none; + cursor: pointer; +} + +.avatar-crop-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--header-primary); + cursor: pointer; + border: none; +} + +.avatar-crop-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--header-primary); + cursor: pointer; + border: none; +} + +.avatar-crop-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 0 20px 20px; +} + +/* ============================================ + USER SETTINGS - MIC LEVEL METER + ============================================ */ +.mic-level-bar { + flex: 1; + height: 8px; + background-color: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; +} + +.mic-level-fill { + height: 100%; + background-color: #3ba55c; + border-radius: 4px; + transition: width 0.05s ease; +} + +/* ============================================ + USER SETTINGS - VOICE SLIDER + ============================================ */ +.voice-slider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 6px; + background: var(--bg-tertiary); + border-radius: 3px; + outline: none; + cursor: pointer; +} + +.voice-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--header-primary); + cursor: pointer; + border: none; +} + +.voice-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--header-primary); + cursor: pointer; + border: none; } \ No newline at end of file diff --git a/Frontend/Electron/src/pages/Chat.jsx b/Frontend/Electron/src/pages/Chat.jsx index 6174707..f35d4b0 100644 --- a/Frontend/Electron/src/pages/Chat.jsx +++ b/Frontend/Electron/src/pages/Chat.jsx @@ -9,6 +9,7 @@ import FriendsView from '../components/FriendsView'; import MembersList from '../components/MembersList'; import ChatHeader from '../components/ChatHeader'; import { useToasts } from '../components/Toast'; +import { PresenceProvider } from '../contexts/PresenceContext'; const Chat = () => { const [view, setView] = useState('server'); @@ -230,25 +231,37 @@ const Chat = () => { ); } + if (!userId) { + return ( + <div className="app-container"> + <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#b9bbbe' }}> + Loading... + </div> + </div> + ); + } + return ( - <div className="app-container"> - <Sidebar - channels={channels} - activeChannel={activeChannel} - onSelectChannel={handleSelectChannel} - username={username} - channelKeys={channelKeys} - view={view} - onViewChange={setView} - onOpenDM={openDM} - activeDMChannel={activeDMChannel} - setActiveDMChannel={setActiveDMChannel} - dmChannels={dmChannels} - userId={userId} - /> - {renderMainContent()} - <ToastContainer /> - </div> + <PresenceProvider userId={userId}> + <div className="app-container"> + <Sidebar + channels={channels} + activeChannel={activeChannel} + onSelectChannel={handleSelectChannel} + username={username} + channelKeys={channelKeys} + view={view} + onViewChange={setView} + onOpenDM={openDM} + activeDMChannel={activeDMChannel} + setActiveDMChannel={setActiveDMChannel} + dmChannels={dmChannels} + userId={userId} + /> + {renderMainContent()} + <ToastContainer /> + </div> + </PresenceProvider> ); }; diff --git a/Frontend/Electron/src/pages/Login.jsx b/Frontend/Electron/src/pages/Login.jsx index 0d92c47..33b9074 100644 --- a/Frontend/Electron/src/pages/Login.jsx +++ b/Frontend/Electron/src/pages/Login.jsx @@ -63,6 +63,22 @@ const Login = () => { localStorage.setItem('publicKey', verifyData.publicKey); } + // Persist session via safeStorage for auto-login on restart + if (window.sessionPersistence) { + try { + await window.sessionPersistence.save({ + userId: verifyData.userId, + username, + publicKey: verifyData.publicKey || '', + signingKey, + privateKey: rsaPriv, + savedAt: Date.now(), + }); + } catch (e) { + console.warn('Session persistence unavailable:', e); + } + } + console.log('Immediate localStorage read check:', localStorage.getItem('userId')); navigate('/chat'); diff --git a/Frontend/Electron/src/styles/themes.css b/Frontend/Electron/src/styles/themes.css index d8279f9..4c770ac 100644 --- a/Frontend/Electron/src/styles/themes.css +++ b/Frontend/Electron/src/styles/themes.css @@ -50,6 +50,7 @@ --border-muted: rgba(255, 255, 255, 0.04); --border-normal: rgba(255, 255, 255, 0.2); --border-strong: rgba(255, 255, 255, 0.44); + --app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); /* Icons */ --icon-default: #dbdee1; @@ -93,6 +94,8 @@ --background-modifier-active: rgba(78, 80, 88, 0.48); --background-modifier-selected: rgba(78, 80, 88, 0.6); --div-border: #1e1f22; + + --text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); } @@ -138,6 +141,7 @@ --border-muted: rgba(0, 0, 0, 0.2); --border-normal: rgba(0, 0, 0, 0.36); --border-strong: rgba(0, 0, 0, 0.48); + --app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); /* Icons */ --icon-default: #313338; @@ -181,6 +185,8 @@ --background-modifier-active: rgba(116, 124, 138, 0.22); --background-modifier-selected: rgba(116, 124, 138, 0.30); --div-border: #e1e2e4; + + --text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); } @@ -198,7 +204,7 @@ --chat-background: #202225; --channeltextarea-background: #252529; --modal-background: #292b2f; - --panel-bg: #1a1b1e; + --panel-bg: color-mix(in oklab, hsl(240 calc(1*5.882%) 13.333% /1) 100%, #000 0%); --embed-background: #242529; /* Text */ @@ -226,6 +232,7 @@ --border-muted: rgba(255, 255, 255, 0.04); --border-normal: rgba(255, 255, 255, 0.2); --border-strong: rgba(255, 255, 255, 0.44); + --app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); /* Icons */ --icon-default: #dddfe4; @@ -254,7 +261,7 @@ /* Compatibility aliases */ --bg-primary: #202225; --bg-secondary: #1a1b1e; - --bg-tertiary: #111214; + --bg-tertiary: #121214; --text-normal: #dddfe4; --header-primary: #f5f5f7; --header-secondary: #a0a4ad; @@ -269,6 +276,8 @@ --background-modifier-active: rgba(78, 80, 88, 0.3); --background-modifier-selected: rgba(78, 80, 88, 0.4); --div-border: #111214; + + --text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); } @@ -314,6 +323,7 @@ --border-muted: rgba(255, 255, 255, 0.16); --border-normal: rgba(255, 255, 255, 0.24); --border-strong: rgba(255, 255, 255, 0.44); + --app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); /* Icons */ --icon-default: #e0def0; @@ -357,4 +367,6 @@ --background-modifier-active: rgba(78, 73, 106, 0.36); --background-modifier-selected: rgba(78, 73, 106, 0.48); --div-border: #080810; + + --text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); } diff --git a/TODO.md b/TODO.md index 4b4726d..12174d4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,16 @@ -- Create auto updater for app -- Save app x and y position on close to a settings.json file -- Save app width and height on close to a settings.json file -- Save app theme on close to a settings.json file +When i scroll up one time with my scroll wheel and move my mouse it scrolls down to the start. + + +- 955px + +<!-- - When you upload your avatar lets make it so they can resize it and zoom in where they want in the photo and show them a circle like discord so they can see how it will look. --> + +- Make it so we can see the avatar in the user control info also. + +- Make it so the server-pill active is actually touching the left of the server-list because right now its not all the way to the left. + + +In our server header we have a server-header-chevron. I want to replace that with invite_user.svg. This will open the invite modal. Use the frontend design skill to help with that. To access the server settings we will click on the server name in that server header. Make it so the name will not go past the invite_user.svg icon and will ellipsis if it needs to. \ No newline at end of file diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 4cf90aa..324659a 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -17,6 +17,7 @@ import type * as gifs from "../gifs.js"; import type * as invites from "../invites.js"; import type * as members from "../members.js"; import type * as messages from "../messages.js"; +import type * as presence from "../presence.js"; import type * as reactions from "../reactions.js"; import type * as roles from "../roles.js"; import type * as typing from "../typing.js"; @@ -39,6 +40,7 @@ declare const fullApi: ApiFromModules<{ invites: typeof invites; members: typeof members; messages: typeof messages; + presence: typeof presence; reactions: typeof reactions; roles: typeof roles; typing: typeof typing; @@ -72,4 +74,67 @@ export declare const internal: FilterApi< FunctionReference<any, "internal"> >; -export declare const components: {}; +export declare const components: { + presence: { + public: { + disconnect: FunctionReference< + "mutation", + "internal", + { sessionToken: string }, + null + >; + heartbeat: FunctionReference< + "mutation", + "internal", + { + interval?: number; + roomId: string; + sessionId: string; + userId: string; + }, + { roomToken: string; sessionToken: string } + >; + list: FunctionReference< + "query", + "internal", + { limit?: number; roomToken: string }, + Array<{ + data?: any; + lastDisconnected: number; + online: boolean; + userId: string; + }> + >; + listRoom: FunctionReference< + "query", + "internal", + { limit?: number; onlineOnly?: boolean; roomId: string }, + Array<{ lastDisconnected: number; online: boolean; userId: string }> + >; + listUser: FunctionReference< + "query", + "internal", + { limit?: number; onlineOnly?: boolean; userId: string }, + Array<{ lastDisconnected: number; online: boolean; roomId: string }> + >; + removeRoom: FunctionReference< + "mutation", + "internal", + { roomId: string }, + null + >; + removeRoomUser: FunctionReference< + "mutation", + "internal", + { roomId: string; userId: string }, + null + >; + updateRoomUser: FunctionReference< + "mutation", + "internal", + { data?: any; roomId: string; userId: string }, + null + >; + }; + }; +}; diff --git a/convex/auth.ts b/convex/auth.ts index 141ab3b..9228b2c 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -198,6 +198,7 @@ export const getPublicKeys = query({ username: v.string(), public_identity_key: v.string(), status: v.optional(v.string()), + displayName: v.optional(v.string()), avatarUrl: v.optional(v.union(v.string(), v.null())), aboutMe: v.optional(v.string()), customStatus: v.optional(v.string()), @@ -215,7 +216,8 @@ export const getPublicKeys = query({ id: u._id, username: u.username, public_identity_key: u.publicIdentityKey, - status: u.status || "online", + status: u.status || "offline", + displayName: u.displayName, avatarUrl, aboutMe: u.aboutMe, customStatus: u.customStatus, @@ -229,6 +231,7 @@ export const getPublicKeys = query({ export const updateProfile = mutation({ args: { userId: v.id("userProfiles"), + displayName: v.optional(v.string()), aboutMe: v.optional(v.string()), avatarStorageId: v.optional(v.id("_storage")), customStatus: v.optional(v.string()), @@ -236,6 +239,7 @@ export const updateProfile = mutation({ returns: v.null(), handler: async (ctx, args) => { const patch: Record<string, unknown> = {}; + if (args.displayName !== undefined) patch.displayName = args.displayName; if (args.aboutMe !== undefined) patch.aboutMe = args.aboutMe; if (args.avatarStorageId !== undefined) patch.avatarStorageId = args.avatarStorageId; if (args.customStatus !== undefined) patch.customStatus = args.customStatus; diff --git a/convex/convex.config.ts b/convex/convex.config.ts new file mode 100644 index 0000000..5a80ba4 --- /dev/null +++ b/convex/convex.config.ts @@ -0,0 +1,6 @@ +import { defineApp } from "convex/server"; +import presence from "@convex-dev/presence/convex.config.js"; + +const app = defineApp(); +app.use(presence); +export default app; diff --git a/convex/dms.ts b/convex/dms.ts index f1a3499..96932d9 100644 --- a/convex/dms.ts +++ b/convex/dms.ts @@ -79,7 +79,7 @@ export const listDMs = query({ channel_name: channel.name, other_user_id: otherUser._id as string, other_username: otherUser.username, - other_user_status: otherUser.status || "online", + other_user_status: otherUser.status || "offline", }; }) ); diff --git a/convex/members.ts b/convex/members.ts index e8ff7e0..1cf6aed 100644 --- a/convex/members.ts +++ b/convex/members.ts @@ -50,7 +50,7 @@ export const getChannelMembers = query({ members.push({ id: user._id, username: user.username, - status: user.status || "online", + status: user.status || "offline", roles: roles.sort((a, b) => b.position - a.position), avatarUrl, aboutMe: user.aboutMe, diff --git a/convex/presence.ts b/convex/presence.ts new file mode 100644 index 0000000..ddd7da8 --- /dev/null +++ b/convex/presence.ts @@ -0,0 +1,32 @@ +import { mutation, query } from "./_generated/server"; +import { components } from "./_generated/api"; +import { v } from "convex/values"; +import { Presence } from "@convex-dev/presence"; + +const presence = new Presence(components.presence); + +export const heartbeat = mutation({ + args: { + roomId: v.string(), + userId: v.string(), + sessionId: v.string(), + interval: v.number(), + }, + handler: async (ctx, { roomId, userId, sessionId, interval }) => { + return await presence.heartbeat(ctx, roomId, userId, sessionId, interval); + }, +}); + +export const list = query({ + args: { roomToken: v.string() }, + handler: async (ctx, { roomToken }) => { + return await presence.list(ctx, roomToken); + }, +}); + +export const disconnect = mutation({ + args: { sessionToken: v.string() }, + handler: async (ctx, { sessionToken }) => { + return await presence.disconnect(ctx, sessionToken); + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 89c4988..f97950f 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -12,6 +12,7 @@ export default defineSchema({ encryptedPrivateKeys: v.string(), isAdmin: v.boolean(), status: v.optional(v.string()), + displayName: v.optional(v.string()), avatarStorageId: v.optional(v.id("_storage")), aboutMe: v.optional(v.string()), customStatus: v.optional(v.string()), diff --git a/discord-html-copy/Settings Panel/settings snippit.txt b/discord-html-copy/Settings Panel/settings snippit.txt new file mode 100644 index 0000000..7e70f7e --- /dev/null +++ b/discord-html-copy/Settings Panel/settings snippit.txt @@ -0,0 +1,3 @@ +<div data-mana-component="layer-modal" class="container__8a031 modal_e44912 theme-dark theme-darker images-dark" style="opacity: 1; transform: scale(1);"><div class="modalContent_e44912"><div class="theme-dark theme-darker images-dark modalContentInner_e44912"><div class="container_abd9a8"><div class="sidebar__409aa theme-dark theme-darker images-dark"><div class="fixedContent__409aa"><div><ul class="section__409aa"><li data-settings-sidebar-item="profile_panel" class="itemContainer_caf372"><div class="item_caf372" role="button" tabindex="0"><div class="profileCustomizationTab__99464"><div class="wrapper__44b0c" role="img" aria-label="Avatar" aria-hidden="false" style="width: 48px; height: 48px;"><svg width="48" height="48" viewBox="0 0 48 48" class="mask__44b0c svg__44b0c" aria-hidden="true"><foreignObject x="0" y="0" width="48" height="48" mask="url(#svg-mask-avatar-default)"><div class="avatarStack__44b0c"><img alt=" " class="avatar__44b0c" aria-hidden="true" src="https://cdn.discordapp.com/avatars/151822040259756033/4558a571cb2156c9a15c3834f663ac48.webp?size=48"></div></foreignObject></svg></div><div class="textContainer__99464"><div class="lineClamp1__4bd52 text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: var(--text-strong);">Moyettes</div><div class="editProfilesRow__99464"><div class="lineClamp1__4bd52 text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: currentcolor;">Edit Profiles</div><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" class=""></path></svg></div></div></div><div class="itemContent_caf372"></div></div></li></ul></div><div class="searchBarContainer__409aa"><div class="container__5a838" data-layout="vertical"><div class="control__5a838"><div class="container__72c38" data-full-width="true"><div class="wrapper__72c38 container__0f084 md__0f084 text-md/normal__0f084 hasLeading__0f084" data-error="false" data-disabled="false"><div class="icon__0f084"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24"><path fill="var(--icon-strong)" fill-rule="evenodd" d="M15.62 17.03a9 9 0 1 1 1.41-1.41l4.68 4.67a1 1 0 0 1-1.42 1.42l-4.67-4.68ZM17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z" clip-rule="evenodd" class=""></path></svg></div><input class="input__0f084" placeholder="Search" data-mana-component="text-input" aria-label="Search" id="«r5ev»" aria-invalid="false" type="text" value="" name=""></div></div></div></div></div></div><div class="navScroller__409aa thin_d125d2 scrollerBase_d125d2 fade_d125d2" dir="ltr" role="list" tabindex="0" data-list-id="settings-sidebar" aria-orientation="vertical" style="overflow: hidden scroll; padding-right: 4px;"><nav class="nav__409aa"><ul class="section__409aa" aria-label="User Settings"><div class="sectionLabel__409aa"><h1 class="heading-sm/medium_cf4812 defaultColor__5345c label__409aa" data-text-variant="heading-sm/medium" style="color: var(--text-muted);">User Settings</h1></div><div data-settings-sidebar-item="account_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___account_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM11.53 11A9.53 9.53 0 0 0 2 20.53c0 .81.66 1.47 1.47 1.47h.22c.24 0 .44-.17.5-.4.29-1.12.84-2.17 1.32-2.91.14-.21.43-.1.4.15l-.26 2.61c-.02.3.2.55.5.55h11.7a.5.5 0 0 0 .5-.55l-.27-2.6c-.02-.26.27-.37.41-.16.48.74 1.03 1.8 1.32 2.9.06.24.26.41.5.41h.22c.81 0 1.47-.66 1.47-1.47A9.53 9.53 0 0 0 12.47 11h-.94Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">My Account</div></div></div></div><div data-settings-sidebar-item="content_and_social_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___content_and_social_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M20.3 5.41h-.39c-.84 0-1.52-.65-1.52-1.46v-.3c0-.9-.77-1.65-1.71-1.65H7.31c-.94 0-1.71.74-1.71 1.65v.3c0 .81-.68 1.46-1.52 1.46H3.7c-.94 0-1.7.73-1.7 1.64v3.52l.01.49c.05 3.11.94 4.69 2.92 6.63C6.72 19.46 11.58 22 11.99 22c.41 0 5.27-2.54 7.06-4.31 1.98-1.95 2.92-3.53 2.92-6.63L22 7.05c0-.9-.76-1.64-1.7-1.64Zm-8.32.03a3.15 3.15 0 1 1-.01 6.3 3.15 3.15 0 0 1 .01-6.3Zm4.52 11.67c-.97.68-2.86 1.62-3.87 2.11-.42.2-.91.2-1.33 0a40.17 40.17 0 0 1-3.82-2.1.87.87 0 0 1-.37-.85c.42-2.69 2.46-3.21 4.89-3.21 2.43 0 4.4.68 4.87 3.08a.97.97 0 0 1-.38.98l.01-.01Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Content & Social</div></div></div></div><div data-settings-sidebar-item="data_and_privacy_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___data_and_privacy_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M20.3 5.41h-.39c-.84 0-1.52-.65-1.52-1.46v-.3c0-.9-.77-1.65-1.71-1.65H7.31c-.94 0-1.71.74-1.71 1.65v.3c0 .81-.68 1.46-1.52 1.46H3.7c-.94 0-1.7.73-1.7 1.64v3.52l.01.49c.05 3.11.94 4.69 2.92 6.63C6.72 19.46 11.58 22 11.99 22c.41 0 5.27-2.54 7.06-4.31 1.98-1.95 2.92-3.53 2.92-6.63L22 7.05c0-.9-.76-1.64-1.7-1.64Zm-7.48 5.03c-.18.07-.29.25-.23.44l.6 4.65a.46.46 0 0 1-.45.51h-1.45a.46.46 0 0 1-.45-.51l.6-4.65c.05-.17-.02-.37-.19-.44a2.08 2.08 0 1 1 1.57 0Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Data & Privacy</div></div></div></div><div data-settings-sidebar-item="family_center_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___family_center_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 3.08c.01.06.06.1.12.1ZM18.44 17.27c.15.43.54.73 1 .73h1.06c.83 0 1.5-.67 1.5-1.5a7.5 7.5 0 0 0-6.5-7.43c-.55-.08-.99.38-1.1.92-.06.3-.15.6-.26.87-.23.58-.05 1.3.47 1.63a9.53 9.53 0 0 1 3.83 4.78ZM12.5 9a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM2 20.5a7.5 7.5 0 0 1 15 0c0 .83-.67 1.5-1.5 1.5a.2.2 0 0 1-.2-.16c-.2-.96-.56-1.87-.88-2.54-.1-.23-.42-.15-.42.1v2.1a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2.1c0-.25-.31-.33-.42-.1-.32.67-.67 1.58-.88 2.54a.2.2 0 0 1-.2.16A1.5 1.5 0 0 1 2 20.5Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Family Center</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="authorized_apps_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___authorized_apps_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M2.06 7.61c-.25.95.31 1.92 1.26 2.18l4.3 1.15c.94.25 1.91-.31 2.17-1.26l1.15-4.3c.25-.94-.31-1.91-1.26-2.17l-4.3-1.15c-.94-.25-1.91.31-2.17 1.26l-1.15 4.3ZM12.98 7.87a2 2 0 0 0 1.75 2.95H20a2 2 0 0 0 1.76-2.95l-2.63-4.83a2 2 0 0 0-3.51 0l-2.63 4.83ZM5.86 13.27a.89.89 0 0 1 1.28 0l.75.77a.9.9 0 0 0 .54.26l1.06.12c.5.06.85.52.8 1.02l-.13 1.08c-.02.2.03.42.14.6l.56.92c.27.43.14 1-.28 1.26l-.9.58a.92.92 0 0 0-.37.48l-.36 1.02a.9.9 0 0 1-1.15.57l-1-.36a.89.89 0 0 0-.6 0l-1 .36a.9.9 0 0 1-1.15-.57l-.36-1.02a.92.92 0 0 0-.37-.48l-.9-.58a.93.93 0 0 1-.28-1.26l.56-.93c.11-.17.16-.38.14-.59l-.12-1.08c-.06-.5.3-.96.8-1.02l1.05-.12a.9.9 0 0 0 .54-.26l.75-.77ZM18.52 13.71a1.1 1.1 0 0 0-2.04 0l-.46 1.24c-.19.5-.57.88-1.07 1.07l-1.24.46a1.1 1.1 0 0 0 0 2.04l1.24.46c.5.19.88.57 1.07 1.07l.46 1.24c.35.95 1.7.95 2.04 0l.46-1.24c.19-.5.57-.88 1.07-1.07l1.24-.46a1.1 1.1 0 0 0 0-2.04l-1.24-.46a1.8 1.8 0 0 1-1.07-1.07l-.46-1.24Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Authorized Apps</div></div></div></div><div data-settings-sidebar-item="sessions_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___sessions_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M3 15.5V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v.5a.5.5 0 0 1-.5.5H17a4 4 0 0 0-4 4v4.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5ZM12.5 18H2a1 1 0 1 0 0 2h10.48c.33 0 .57-.3.54-.63A4.08 4.08 0 0 1 13 19v-.5a.5.5 0 0 0-.5-.5Z" class=""></path><path fill="currentColor" fill-rule="evenodd" d="M15 11c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-8Zm2 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Devices</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="connections_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___connections_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M16.32 14.72a1 1 0 0 1 0-1.41l2.51-2.51a3.98 3.98 0 0 0-5.62-5.63l-2.52 2.51a1 1 0 0 1-1.41-1.41l2.52-2.52a5.98 5.98 0 0 1 8.45 8.46l-2.52 2.51a1 1 0 0 1-1.41 0ZM7.68 9.29a1 1 0 0 1 0 1.41l-2.52 2.51a3.98 3.98 0 1 0 5.63 5.63l2.51-2.52a1 1 0 0 1 1.42 1.42l-2.52 2.51a5.98 5.98 0 0 1-8.45-8.45l2.51-2.51a1 1 0 0 1 1.42 0Z" class=""></path><path fill="currentColor" d="M14.7 10.7a1 1 0 0 0-1.4-1.4l-4 4a1 1 0 1 0 1.4 1.4l4-4Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Connections</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="notifications_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___notifications_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9.7 2.89c.18-.07.32-.24.37-.43a2 2 0 0 1 3.86 0c.05.2.19.36.38.43A7 7 0 0 1 19 9.5v2.09c0 .12.05.24.13.33l1.1 1.22a3 3 0 0 1 .77 2.01v.28c0 .67-.34 1.29-.95 1.56-1.31.6-4 1.51-8.05 1.51-4.05 0-6.74-.91-8.05-1.5-.61-.28-.95-.9-.95-1.57v-.28a3 3 0 0 1 .77-2l1.1-1.23a.5.5 0 0 0 .13-.33V9.5a7 7 0 0 1 4.7-6.61ZM9.18 19.84A.16.16 0 0 0 9 20a3 3 0 1 0 6 0c0-.1-.09-.17-.18-.16a24.86 24.86 0 0 1-5.64 0Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Notifications</div></div></div></div></ul><ul class="section__409aa" aria-label="Billing Settings"><div class="sectionLabel__409aa"><h1 class="heading-sm/medium_cf4812 defaultColor__5345c label__409aa" data-text-variant="heading-sm/medium" style="color: var(--text-muted);">Billing Settings</h1></div><div data-settings-sidebar-item="nitro_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___nitro_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M16.23 12c0 1.29-.95 2.25-2.22 2.25A2.18 2.18 0 0 1 11.8 12c0-1.29.95-2.25 2.22-2.25 1.27 0 2.22.96 2.22 2.25ZM23 12c0 5.01-4 9-8.99 9a8.93 8.93 0 0 1-8.75-6.9H3.34l-.9-4.2H5.3c.26-.96.68-1.89 1.21-2.7H1.89L1 3h12.74C19.13 3 23 6.99 23 12Zm-4.26 0c0-2.67-2.1-4.8-4.73-4.8A4.74 4.74 0 0 0 9.28 12c0 2.67 2.1 4.8 4.73 4.8a4.74 4.74 0 0 0 4.73-4.8Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Nitro</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="premium_guild_subscriptions_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___premium_guild_subscriptions_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M11.47 6.55a.75.75 0 0 1 1.06 0l2.2 2.14c.14.14.23.34.23.55v5.52c0 .21-.09.4-.23.55l-2.2 2.14a.75.75 0 0 1-1.06 0l-2.2-2.14a.77.77 0 0 1-.23-.55V9.24c0-.21.09-.41.23-.55l2.2-2.14Z" class=""></path><path fill="currentColor" fill-rule="evenodd" d="M10.95 1.43a1.5 1.5 0 0 1 2.1 0l5.49 5.33c.3.29.46.68.46 1.1v8.44c-.04.35-.2.69-.46.94l-5.49 5.33-.11.1a1.5 1.5 0 0 1-2-.1l-5.48-5.33c-.3-.29-.46-.68-.46-1.1V7.86c0-.42.17-.81.46-1.1l5.49-5.33ZM7.29 7.76c-.2.2-.3.46-.3.73v7.02c0 .27.1.54.3.73l4 3.9a1 1 0 0 0 1.41 0l4-3.9c.2-.2.31-.46.31-.73V8.49c0-.27-.1-.54-.3-.73l-4-3.9a1 1 0 0 0-1.41 0l-4 3.9Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Server Boost</div></div></div></div><div data-settings-sidebar-item="subscriptions_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___subscriptions_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path d="M22.95 11.05a1 1 0 0 1 1 1 11.5 11.5 0 0 1-19.43 8.33l-.08.08v2.2a1.1 1.1 0 0 1-.68 1 1.1 1.1 0 0 1-1.19-.23 1.1 1.1 0 0 1-.32-.78v-4.8a1.1 1.1 0 0 1 .52-.94l.03-.01c.08-.05.17-.09.25-.11l.05-.01.24-.03h4.82a1.09 1.09 0 0 1 .2.02h.02a1.1 1.1 0 0 1 .18.06l.03.01.07.03.02.02.06.03.03.02.04.03a1.1 1.1 0 0 1 .44.88l-.01.15V18a1.1 1.1 0 0 1-.3.61l-.12.1-.03.02a1.09 1.09 0 0 1-.63.2l-2.18-.02-.04.04a9.5 9.5 0 0 0 16.01-6.91 1 1 0 0 1 1-1Z" fill="currentColor" class=""></path><path fill-rule="evenodd" d="M13.41 7.2c1.1.15 1.96.72 2.59 1.7l-1.35 1.09c-.38-.6-.8-1-1.24-1.18v2.24c.86.16 1.53.45 2.02.86.5.4.74 1.02.74 1.84 0 .81-.25 1.48-.77 1.98-.5.5-1.17.81-1.99.94v1.2h-1.38v-1.18a4.39 4.39 0 0 1-1.83-.58c-.5-.31-.9-.71-1.2-1.2l1.41-1.22c.44.7.98 1.12 1.62 1.28v-2.4a4.19 4.19 0 0 1-1.91-.9 2.32 2.32 0 0 1-.79-1.83c0-.72.25-1.31.75-1.77a3.4 3.4 0 0 1 1.95-.87V6h1.38v1.2Zm0 7.75c.56-.15.84-.48.84-1.02a.97.97 0 0 0-.2-.64c-.12-.16-.33-.29-.64-.4v2.06ZM12.03 8.8c-.27.08-.48.2-.63.39a.84.84 0 0 0-.23.57c0 .25.07.45.19.6.13.15.36.28.67.39V8.8Z" clip-rule="evenodd" fill="currentColor" class=""></path><path d="M21.16 0a1.1 1.1 0 0 1 1.09 1.1v4.8c0 .3-.12.57-.32.78l-.08.07-.08.06-.02.01-.07.05-.03.01-.05.02-.06.03h-.03a1.12 1.12 0 0 1-.11.04l-.04.01h-.05l-.06.02H16.35a1.1 1.1 0 0 1-1.02-1.52 1.1 1.1 0 0 1 .35-.45l.02-.02a1.1 1.1 0 0 1 .64-.2l2.18.02.07-.07A9.5 9.5 0 0 0 3 12.05a1 1 0 1 1-2 0 11.5 11.5 0 0 1 19-8.71l.06-.05v-2.2a1.1 1.1 0 0 1 .68-1c.13-.06.27-.1.42-.09Z" fill="currentColor" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Subscriptions</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="gift_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___gift_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M4 6a4 4 0 0 1 4-4h.09c1.8 0 3.39 1.18 3.91 2.9A4.09 4.09 0 0 1 15.91 2H16a4 4 0 0 1 3.46 6H20a2 2 0 0 1 2 2v1.5a.5.5 0 0 1-.5.5h-19a.5.5 0 0 1-.5-.5V10c0-1.1.9-2 2-2h.54A3.98 3.98 0 0 1 4 6Zm12 2a2 2 0 1 0 0-4h-.09c-.96 0-1.8.65-2.02 1.58L13.29 8H16Zm-5.89-2.42.6 2.42H8a2 2 0 1 1 0-4h.09c.96 0 1.8.65 2.02 1.58Z" clip-rule="evenodd" class=""></path><path fill="currentColor" d="M3 20c0 1.1.9 2 2 2h5.5a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5h-7a.5.5 0 0 0-.5.5V20ZM13.5 22a.5.5 0 0 1-.5-.5v-7c0-.28.22-.5.5-.5h7c.28 0 .5.22.5.5V20a2 2 0 0 1-2 2h-5.5Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Gift Inventory</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="billing_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___billing_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M1 6a3 3 0 0 1 3-3h16a3 3 0 0 1 3 3v1H1V6Z" class=""></path><path fill="currentColor" fill-rule="evenodd" d="M1 10h22v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-8Zm4 3a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H5Zm-1 4a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Zm13-4a1 1 0 1 0 0 2h2a1 1 0 1 0 0-2h-2Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Billing</div></div></div></div></ul><ul class="section__409aa" aria-label="App Settings"><div class="sectionLabel__409aa"><h1 class="heading-sm/medium_cf4812 defaultColor__5345c label__409aa" data-text-variant="heading-sm/medium" style="color: var(--text-muted);">App Settings</h1></div><div data-settings-sidebar-item="appearance_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___appearance_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M19 16h-5a2 2 0 0 0-2 2v2c0 1.66-1.37 3.04-2.96 2.6A11 11 0 1 1 23 12c0 2.2-2 4-4 4ZM13.5 4.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM17.25 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-9-1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm-3.75 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Appearance</div></div><div class="itemContent_caf372"></div></div></div><div data-settings-sidebar-item="accessibility_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___accessibility_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm-2.06-3.65a1 1 0 0 1-1.88-.7l1.9-5.08a.5.5 0 0 0 .04-.18v-2.22a.5.5 0 0 0-.38-.48l-2.86-.72a1 1 0 0 1 .48-1.94l3.55.89a5 5 0 0 0 2.42 0l3.55-.89a1 1 0 1 1 .48 1.94l-2.86.72a.5.5 0 0 0-.38.48v2.22l.03.18 1.9 5.08a1 1 0 0 1-1.87.7l-1.6-4.25a.5.5 0 0 0-.93 0l-1.6 4.25ZM14 6a2 2 0 0 0-2-2 2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Accessibility</div></div></div></div><div data-settings-sidebar-item="voice_and_video_panel" class="itemContainer_caf372"><div class="item_caf372 active_caf372" role="listitem" data-list-item-id="settings-sidebar___voice_and_video_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a4 4 0 0 0-4 4v4a4 4 0 0 0 8 0V6a4 4 0 0 0-4-4Z" class=""></path><path fill="currentColor" d="M6 10a1 1 0 0 0-2 0 8 8 0 0 0 7 7.94V20H9a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2h-2v-2.06A8 8 0 0 0 20 10a1 1 0 1 0-2 0 6 6 0 0 1-12 0Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Voice & Video</div></div></div><div class="subnav_e4d939" role="list" style="--custom-nav-count: 6; --custom-nav-index: 0; --custom-nav-width: 2px;"><div class="track_e4d939" aria-hidden="true"><div class="thumb_e4d939" style="height: 20.0001px; transform: none;"></div></div><div class="thumbAnchor_e4d939" aria-hidden="true"></div><div class="item_e4d939 active_e4d939" role="listitem" data-list-item-id="settings-sidebar___voice_category" tabindex="-1">Voice</div><div class="item_e4d939" role="listitem" data-list-item-id="settings-sidebar___camera_category" tabindex="-1">Camera</div><div class="item_e4d939" role="listitem" data-list-item-id="settings-sidebar___streaming_category" tabindex="-1">Streaming</div><div class="item_e4d939" role="listitem" data-list-item-id="settings-sidebar___sounds_category" tabindex="-1">Sounds</div><div class="item_e4d939" role="listitem" data-list-item-id="settings-sidebar___soundboard_category" tabindex="-1">Soundboard</div><div class="item_e4d939" role="listitem" data-list-item-id="settings-sidebar___voice_and_video_diagnostics_category" tabindex="-1">Advanced</div></div></div><div data-settings-sidebar-item="chat_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___chat_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22a10 10 0 1 0-8.45-4.64c.13.19.11.44-.04.61l-2.06 2.37A1 1 0 0 0 2.2 22H12Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Chat</div></div></div></div><div data-settings-sidebar-item="keybinds_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___keybinds_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M4 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h16a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3H4Zm-.5 3a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm4 0a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM7 11.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM3.5 11a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM11 7.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm.5 3.5a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM15 7.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm.5 3.5a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM19 7.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm.5 3.5a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM7 15.5c0-.28.22-.5.5-.5h9c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Keybinds</div></div></div></div><div data-settings-sidebar-item="language_and_time_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___language_and_time_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M11 2a1 1 0 1 0-2 0v1H3a1 1 0 0 0 0 2h9.94a8.04 8.04 0 0 1-2.76 5.11l-.14.12-.2-.16a7.9 7.9 0 0 1-2.38-3.4 1 1 0 1 0-1.88.67 9.9 9.9 0 0 0 2.92 4.21l-3.15 2.69a1 1 0 0 0 1.3 1.52l3.4-2.91 1.31 1.08a1 1 0 1 0 1.28-1.53l-1.04-.87c1.9-1.68 3.1-4.02 3.35-6.53H17a1 1 0 1 0 0-2h-6V2Z" class=""></path><path fill="currentColor" fill-rule="evenodd" d="M22.77 22H20.5l-.99-2.77H14.3L13.3 22h-2.27l4.72-12.42h2.3L22.77 22ZM16.9 11.87l-1.92 5.43h3.85l-1.93-5.43Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Language & Time</div></div></div></div><div data-settings-sidebar-item="windows_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___windows_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M5 2a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5ZM13.5 20a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-.5.5H9a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2h-1.5Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Windows Settings</div></div></div></div><div data-settings-sidebar-item="streamer_mode_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___streamer_mode_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M2 5a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Zm5 2c0-1.1.9-2 2-2h3a2 2 0 0 1 2 2v.36c0-.21.14-.4.34-.47l2-.67a.5.5 0 0 1 .66.47v4.62a.5.5 0 0 1-.66.47l-2-.67a.5.5 0 0 1-.34-.47V11a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V7Z" clip-rule="evenodd" class=""></path><path fill="currentColor" d="M13 19.5c0 .28.22.5.5.5H15a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2h1.5a.5.5 0 0 0 .5-.5v-2c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v2Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Streamer Mode</div></div></div></div><div data-settings-sidebar-item="advanced_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___advanced_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M4 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm10-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm8 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Advanced</div></div></div></div></ul><ul class="section__409aa" aria-label="Activity Settings"><div class="sectionLabel__409aa"><h1 class="heading-sm/medium_cf4812 defaultColor__5345c label__409aa" data-text-variant="heading-sm/medium" style="color: var(--text-muted);">Activity Settings</h1></div><div data-settings-sidebar-item="activity_privacy_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___activity_privacy_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M17.78 13.1a6.02 6.02 0 0 0-3.51.68 3.92 3.92 0 0 0-1.87 2.56c-.17.7-.4 1.97-.4 3.69V21a1 1 0 0 1-1 1H6.15a.5.5 0 0 1-.5-.55l.27-2.6c.02-.26-.27-.37-.41-.16a10.3 10.3 0 0 0-1.32 2.9.53.53 0 0 1-.5.41h-.22C2.67 22 2 21.38 2 20.59A9.53 9.53 0 0 1 11.53 11h.94c2.03 0 3.92.64 5.47 1.73.18.12.06.4-.16.38ZM12 2a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z" class=""></path><path fill="currentColor" d="M23.66 16.47c.13.57.34 1.69.34 3.23v2.23a1.4 1.4 0 0 1-2.66.63l-.8-1.6a.5.5 0 0 0-.56-.25c-.26.06-.62.12-.98.12s-.72-.06-.98-.12a.5.5 0 0 0-.56.25l-.8 1.6a1.4 1.4 0 0 1-2.66-.63V19.7c0-1.54.2-2.66.34-3.23.31-1.3 1.36-1.55 2.46-1.79.23-.05.46.03.61.18.17.16.36.33.59.32a19.34 19.34 0 0 1 2 0c.23 0 .42-.16.58-.32a.68.68 0 0 1 .62-.18c1.1.24 2.15.5 2.46 1.8Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Activity Privacy</div></div></div></div><div data-settings-sidebar-item="registered_games_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___registered_games_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09 3.09 0 0 1-5.85 1.38l-1.76-3.51a1.09 1.09 0 0 0-1.23-.55c-.57.13-1.36.27-2.16.27s-1.6-.14-2.16-.27c-.49-.11-1 .1-1.23.55l-1.76 3.51A3.09 3.09 0 0 1 1 17.91V13c0-3.38.46-5.85.75-7.1.15-.6.49-1.13 1.04-1.4a.47.47 0 0 0 .24-.44c0-.7.48-1.32 1.2-1.47l2.93-.62c.5-.1 1 .06 1.36.4.35.34.78.71 1.28.68a42.4 42.4 0 0 1 4.4 0c.5.03.93-.34 1.28-.69.35-.33.86-.5 1.36-.39l2.94.62c.7.15 1.19.78 1.19 1.47ZM20 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM15.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 7a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2H7v1a1 1 0 1 1-2 0v-1H4a1 1 0 1 1 0-2h1V7Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Registered Games</div></div></div></div><div data-settings-sidebar-item="overlay_panel" class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___overlay_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M4 5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v.18a1 1 0 1 0 2 0V5a3 3 0 0 0-3-3H5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h.18a1 1 0 1 0 0-2H5a1 1 0 0 1-1-1V5Z" class=""></path><path fill="currentColor" fill-rule="evenodd" d="M8 11a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-8a3 3 0 0 1-3-3v-8Zm2 0a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-8Z" clip-rule="evenodd" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Game Overlay</div></div></div></div></ul><ul class="section__409aa"><div class="itemContainer_caf372"><div class="item_caf372" role="listitem" data-list-item-id="settings-sidebar___logout_sidebar_item" tabindex="-1"><div class="itemContent_caf372"><svg class="icon_caf372" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9 12a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1Z" class=""></path><path fill="currentColor" fill-rule="evenodd" d="M2.75 3.02A3 3 0 0 1 5 2h10a3 3 0 0 1 3 3v7.64c0 .44-.55.7-.95.55a3 3 0 0 0-3.17 4.93l.02.03a.5.5 0 0 1-.35.85h-.05a.5.5 0 0 0-.5.5 2.5 2.5 0 0 1-3.68 2.2l-5.8-3.09A3 3 0 0 1 2 16V5a3 3 0 0 1 .76-1.98Zm1.3 1.95A.04.04 0 0 0 4 5v11c0 .36.2.68.49.86l5.77 3.08a.5.5 0 0 0 .74-.44V8.02a.5.5 0 0 0-.32-.46l-6.63-2.6Z" clip-rule="evenodd" class=""></path><path fill="currentColor" d="M15.3 16.7a1 1 0 0 1 1.4-1.4l4.3 4.29V16a1 1 0 1 1 2 0v6a1 1 0 0 1-1 1h-6a1 1 0 1 1 0-2h3.59l-4.3-4.3Z" class=""></path></svg><div class="text-md/medium_cf4812" data-text-variant="text-md/medium" style="color: currentcolor;">Log Out</div></div></div></div></ul></nav><div><div class="clickable__2debe compact__2debe" aria-describedby="uid_1678" role="button" tabindex="0"><div class="compactInfo__2debe" data-mtctest-ignore="true"><span class="text-xxs/normal_cf4812" data-text-variant="text-xxs/normal" style="color: var(--text-muted);">stable 495347 <span class="versionHash__2debe">(9404403)</span></span><span class="text-xxs/normal_cf4812" data-text-variant="text-xxs/normal" style="color: var(--text-muted);">1.0.9223<span class="appArch__2debe"> x64</span><span> (74638)</span></span></div></div><span style="display: none;"></span><span id="uid_1678" class="hiddenVisually_b18fe2">Click to copy version</span><div class="links__7aac8"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" href="//discord.com/privacy" rel="noreferrer noopener" target="_blank"><span class="text-xxs/normal_cf4812" data-text-variant="text-xxs/normal" style="color: currentcolor;">Privacy Policy</span></a><span class="bullet__7aac8" aria-hidden="true">•</span><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" href="//discord.com/terms" rel="noreferrer noopener" target="_blank"><span class="text-xxs/normal_cf4812" data-text-variant="text-xxs/normal" style="color: currentcolor;">Terms of Service</span></a><span class="bullet__7aac8" aria-hidden="true">•</span><a class="anchor_edefb8 moreButton__7aac8" aria-expanded="false" role="button" tabindex="0"><span class="text-xxs/normal_cf4812" data-text-variant="text-xxs/normal" style="color: currentcolor;">More</span></a></div></div><div aria-hidden="true" style="position: absolute; pointer-events: none; min-height: 0px; min-width: 1px; flex: 0 0 auto; height: 16px;"></div></div> </div><div class="content_e9e3ed"><div class="contentHeader_e9e3ed theme-dark theme-darker images-dark"><div data-align="center" data-justify="start" data-direction="horizontal" data-wrap="false" data-full-width="true" class="stack_dbd263 contentHeaderLeft_e9e3ed" style="gap: var(--space-8); padding: var(--space-0);"><nav aria-label="Breadcrumb" class="breadcrumbsNav__6989f"><div data-align="stretch" data-justify="start" data-direction="horizontal" data-wrap="false" data-full-width="true" class="stack_dbd263 breadcrumbs__6989f" role="list" style="gap: var(--space-8); padding: var(--space-0);"><li aria-current="page" class="breadcrumb__6989f"><div data-align="center" data-justify="start" data-direction="horizontal" data-wrap="false" data-full-width="true" class="stack_dbd263 breadcrumbContent__6989f" style="gap: var(--space-xs); padding: var(--space-0);"><div class="text-md/medium_cf4812 breadcrumbText__6989f" data-text-variant="text-md/medium" style="color: var(--text-default);">Voice & Video</div></div></li></div></nav></div><button data-mana-component="button" role="button" class="button_a22cb0 sm_a22cb0 icon-only_a22cb0" type="button" aria-label="Close"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><svg class="icon_a22cb0" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M19.3 20.7a1 1 0 0 0 1.4-1.4L13.42 12l7.3-7.3a1 1 0 0 0-1.42-1.4L12 10.58l-7.3-7.3a1 1 0 0 0-1.4 1.42L10.58 12l-7.3 7.3a1 1 0 1 0 1.42 1.4L12 13.42l7.3 7.3Z" class=""></path></svg></div></div></button></div><div class="contentBody_e9e3ed"><div class="scroller__6131a auto_d125d2 scrollerBase_d125d2" dir="ltr" data-settings-panel-scroller="true" style="overflow: hidden scroll; padding-right: 0px;"><div class="panel__6131a"><div class="categories__6131a"><div data-debug-key="voice_category" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-left: var(--space-12); padding-right: var(--space-12); padding-bottom: var(--space-24);"><div class="headerTitle__450f6"><h1 class="heading-xl/normal_cf4812 defaultColor__5345c" data-text-variant="heading-xl/normal" style="color: var(--text-strong);">Voice</h1></div></div><div data-settings-category-key="voice_category" aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="voice_input_output_device_split" class="container__75920"><div class="split__678d3"><div data-debug-key="voice_microphone_input_select" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5f4»" for="«r5f5»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Microphone</label></div><div class="control__5a838"><div class="container__72c38 container_a16aea" data-full-width="false" role="button" id="«r5f5»" aria-invalid="false" aria-busy="false" aria-disabled="false" aria-expanded="false" data-size="md" data-variant="filled" aria-haspopup="listbox" tabindex="0"><div class="wrapper__72c38 select_a16aea" data-disabled="false"><div class="defaultColor__4bd52 text-md/medium_cf4812 value_a16aea" data-text-variant="text-md/medium"><div class="deviceContainer__12eef"><div class="lineClamp2Plus__4bd52 text-md/medium_cf4812 deviceLabel__12eef" data-text-variant="text-md/medium" style="color: var(--text-default); -webkit-line-clamp: 2;">Wave Link Stream (Elgato Wave:3)</div></div></div><div class="icons_a16aea"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z" class=""></path></svg></div></div></div></div></div></div></div><div data-debug-key="voice_speakers_output_select" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5fa»" for="«r5fb»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Speaker</label></div><div class="control__5a838"><div class="container__72c38 container_a16aea" data-full-width="false" role="button" id="«r5fb»" aria-invalid="false" aria-busy="false" aria-disabled="false" aria-expanded="false" data-size="md" data-variant="filled" aria-haspopup="listbox" tabindex="0"><div class="wrapper__72c38 select_a16aea" data-disabled="false"><div class="defaultColor__4bd52 text-md/medium_cf4812 value_a16aea" data-text-variant="text-md/medium"><div class="deviceContainer__12eef"><div class="lineClamp2Plus__4bd52 text-md/medium_cf4812 deviceLabel__12eef" data-text-variant="text-md/medium" style="color: var(--text-default); -webkit-line-clamp: 2;">Voice Chat (Elgato Wave:3)</div></div></div><div class="icons_a16aea"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z" class=""></path></svg></div></div></div></div></div></div></div></div></div><div data-debug-key="voice_input_output_volume_split" class="container__75920"><div class="split__678d3"><div data-debug-key="voice_input_volume_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5ff»" for="«r5fg»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Microphone Volume</label></div><div class="control__5a838"><div class="slider_a562c8" id="«r5fg»" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-disabled="false" aria-orientation="horizontal" aria-invalid="false" role="slider" tabindex="0" style="--grabber-size: 16px; --bar-size: 4px;"><div class="track_a562c8"></div><div class="bar_a562c8"><div class="barFill_a562c8" style="width: 100%;"></div></div><div class="track_a562c8"><div class="grabber_a562c8" aria-describedby="«r5fk»" style="left: 100%;"></div><span id="«r5fk»" class="hiddenVisually_b18fe2">100%</span></div></div></div></div></div></div><div data-debug-key="voice_output_volume_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5fl»" for="«r5fm»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Speaker Volume</label></div><div class="control__5a838"><div class="slider_a562c8" id="«r5fm»" aria-valuemin="0" aria-valuemax="200" aria-valuenow="199.99999999999994" aria-disabled="false" aria-orientation="horizontal" aria-invalid="false" role="slider" tabindex="0" style="--grabber-size: 16px; --bar-size: 4px;"><div class="track_a562c8"></div><div class="bar_a562c8"><div class="barFill_a562c8" style="width: 100%;"></div></div><div class="track_a562c8"><div class="grabber_a562c8" aria-describedby="«r5fq»" style="left: 100%;"></div><span id="«r5fq»" class="hiddenVisually_b18fe2">200%</span></div></div></div></div></div></div></div></div><div data-debug-key="voice_microphone_test_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="micTest__169b3"><div class="container__011b7"><div class="container__5a838" data-layout="vertical"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5fs»" for="«r5ft»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Mic Test</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5fv»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Having mic issues? Start a test and say something fun—we'll play your voice back to you.</div></div><div class="control__5a838"><div class="micTest__011b7"><div class="buttonSizer__011b7" aria-hidden="true"><button data-mana-component="button" role="button" class="button_a22cb0 sm_a22cb0 primary_a22cb0 hasText_a22cb0" type="button"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><span class="lineClamp1__4bd52 text-sm/medium_cf4812" data-text-variant="text-sm/medium">Stop Testing</span></div></div></button><div class="buttonSizerSpacer__011b7"></div></div><div class="buttonWrapper__011b7" aria-describedby="«r5g1»" style="min-width: 98.375px;"><button data-mana-component="button" role="button" class="button_a22cb0 sm_a22cb0 primary_a22cb0 hasText_a22cb0 fullWidth_a22cb0" type="button"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><span class="lineClamp1__4bd52 text-sm/medium_cf4812" data-text-variant="text-sm/medium">Let's Check</span></div></div></button></div><span id="«r5g1»" class="hiddenVisually_b18fe2">You'll be muted and deafened in your voice session during Mic Testing.</span><div class="wrapper__06283"><div class="container__06283" style="width: 568px; background: none;"><div class="progress__06283" style="transform: translateX(0px);"></div><svg class="notches__06283 gray__06283" aria-hidden="true" role="img" width="568" height="20" viewBox="0 0 568 20"><pattern id="pill-frame-pattern" width="0.014084507042253521" height="1"><path d="m0 0h8v20h-8zm4 2c-1.1045695 0-2 .8954305-2 2v12c0 1.1045695.8954305 2 2 2s2-.8954305 2-2v-12c0-1.1045695-.8954305-2-2-2z" fill-rule="evenodd" fill="currentColor"></path></pattern><rect fill="url(#pill-frame-pattern)" height="100%" width="100%"></rect></svg></div><div class="text-xs/normal_cf4812 micTestCaption__011b7" data-text-variant="text-xs/normal" style="color: var(--text-subtle);"></div></div></div></div></div></div></div><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Need help with voice or video? Check out our <a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" href="https://support.discord.com/hc/en-us/articles/360045138471?utm_source=discord&utm_medium=blog&utm_campaign=2020-06_help-voice-video&utm_content=--t%3Apm" rel="noreferrer noopener" target="_blank">troubleshooting guide</a>.</div></div></div></div></div><div class="divider__1de9c divider__143d2 divider__6131a"></div><div data-debug-key="voice_input_profile_category" class="container__75920"><div aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="voice_input_profile_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5g3»" for="«r5g4»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Input Profile</label></div><div class="control__5a838"><div id="«r5g4»" data-mana-component="BaseRadioGroup" class="group__64e61" data-rac="" aria-labelledby="react-aria3688859976-«r5g9» «r5g3»" role="radiogroup" aria-orientation="vertical" data-orientation="vertical"><label data-react-aria-pressable="true" class="radioGroupOption__64e61" data-rac=""><span style="border: 0px; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"><input data-react-aria-pressable="true" tabindex="-1" type="radio" value="VOICE_ISOLATION" name="react-aria3688859976-«r5gd»" title=""></span><svg class="radioIndicator__64e61" width="20" height="20" viewBox="0 0 40 40" fill="none" shape-rendering="geometricPrecision"><circle cx="20" cy="20" r="20" class="outerRadioBase__64e61"></circle><circle cx="20" cy="20" r="20" class="outerRadioFill__64e61"></circle><circle cx="20" cy="20" r="8" class="innerDotRadio__64e61"></circle></svg><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-4); padding: var(--space-0);"><span class="label__64e61" id="react-aria3688859976-«r5g9»"><div class="defaultColor__4bd52 text-md/normal_cf4812" data-text-variant="text-md/normal">Voice Isolation</div></span><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Just your beautiful voice: let Discord cut through the noise</div></div></label><label data-react-aria-pressable="true" class="radioGroupOption__64e61" data-rac=""><span style="border: 0px; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"><input data-react-aria-pressable="true" tabindex="-1" type="radio" value="STUDIO" name="react-aria3688859976-«r5gd»" title=""></span><svg class="radioIndicator__64e61" width="20" height="20" viewBox="0 0 40 40" fill="none" shape-rendering="geometricPrecision"><circle cx="20" cy="20" r="20" class="outerRadioBase__64e61"></circle><circle cx="20" cy="20" r="20" class="outerRadioFill__64e61"></circle><circle cx="20" cy="20" r="8" class="innerDotRadio__64e61"></circle></svg><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-4); padding: var(--space-0);"><span class="label__64e61" id="react-aria3688859976-«r5g9»"><div class="defaultColor__4bd52 text-md/normal_cf4812" data-text-variant="text-md/normal">Studio</div></span><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Pure audio: open mic with no processing</div></div></label><label data-react-aria-pressable="true" class="radioGroupOption__64e61" data-rac="" data-selected="true"><span style="border: 0px; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"><input data-react-aria-pressable="true" tabindex="0" type="radio" value="CUSTOM" checked="" name="react-aria3688859976-«r5gd»" title=""></span><svg class="radioIndicator__64e61" width="20" height="20" viewBox="0 0 40 40" fill="none" shape-rendering="geometricPrecision"><circle cx="20" cy="20" r="20" class="outerRadioBase__64e61"></circle><circle cx="20" cy="20" r="20" class="outerRadioFill__64e61"></circle><circle cx="20" cy="20" r="8" class="innerDotRadio__64e61"></circle></svg><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-4); padding: var(--space-0);"><span class="label__64e61" id="react-aria3688859976-«r5g9»"><div class="defaultColor__4bd52 text-md/normal_cf4812" data-text-variant="text-md/normal">Custom</div></span><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Advanced mode: give me all the buttons and dials!</div></div></label></div></div></div></div></div><div data-debug-key="voice_input_sensitivity_field_set" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><fieldset aria-describedby="«r5gf»"><legend tag="legend" id="«r5ge»" class="hiddenVisually_b18fe2">Input Sensitivity Settings</legend><span id="«r5gf»" class="hiddenVisually_b18fe2">Controls how much sound Discord transmits from your mic. If the indicator is green, then Discord is transmitting your beautiful voice.</span><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding: var(--space-0);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5gg»" for="«r5gh»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Automatically Adjust Input Sensitivity</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5gj»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Controls how much sound Discord transmits from your mic. If the indicator is green, then Discord is transmitting your beautiful voice.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5gh»" aria-describedby="«r5gj»" aria-labelledby="«r5gg»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div><div class="inputSensitivitySlider__39fa9 autoThresholdSlider__39fa9" aria-describedby="«r5gf»" aria-labelledby="«r5ge»"><div class="inputSensitivityBar__39fa9 sliderBar__39fa9 speaking__39fa9"></div></div></div></fieldset></div></div><div data-debug-key="voice_noise_suppression_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding: var(--space-0);"><div class="container__5a838" data-layout="horizontal" style="--custom-field-horizontal-control-width: min(248px, auto);"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5gp»" for="«r5gq»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Noise Suppression</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5gs»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Reduces background noise from your mic. Powered by <a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" href="https://support.discord.com/hc/en-us/articles/360040843952" rel="noreferrer noopener" target="_blank">Krisp</a>.</div></div><div class="control__5a838"><div style="width: 100%;"><div class="container__72c38 selectFieldContainer__0edde fullWidth__0edde" data-full-width="true" data-mana-component="select-input-field" aria-busy="false"><div class="wrapper__72c38 selectField__0edde"><div class="selectFieldContent__0edde"><div class="selectButton__0edde"><div class="focusTarget__54e4b" aria-label="Noise Suppression" aria-describedby="«r5gs»" role="button" tabindex="0" style="margin-left: -4px;"></div><span class="hiddenVisually_b18fe2">Noise Suppression, </span><div class="listBoxItemContent__2e223 option__56a50 inInput__2e223"><div class="lineClamp1__4bd52 text-md/normal_cf4812" data-text-variant="text-md/normal" style="color: currentcolor; grid-column: 1 / 3;">Krisp</div></div></div></div><span></span><button aria-hidden="true" tabindex="-1" class="chevronButton__0edde"><span class="chevronIcon__0edde"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M3.3 8.3a1 1 0 0 1 1.4 0l7.3 7.29 7.3-7.3a1 1 0 1 1 1.4 1.42l-8 8a1 1 0 0 1-1.4 0l-8-8a1 1 0 0 1 0-1.42Z" class=""></path></svg></span></button></div></div></div></div></div><img width="48" height="32" alt="" src="/assets/b46488e4f6a1d7ef.svg"></div></div></div><div data-debug-key="voice_echo_cancellation_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5gu»" for="«r5gv»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Echo Cancellation</label></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5gv»" aria-labelledby="«r5gu»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="voice_push_to_talk_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5h3»" for="«r5h4»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Push to Talk</label></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5h4»" aria-labelledby="«r5h3»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="input_profile_voice_advanced_accordion" class="container__75920"><div class="react-aria-Disclosure" data-rac=""><div class="baseControlItem_dbfbe0 clickable_dbfbe0" role="button" tabindex="0" style="min-height: 72px;"><div class="baseControlItemContent_dbfbe0"><div class="baseControlItemTitle_dbfbe0"><h1 class="heading-md/medium_cf4812 defaultColor__5345c" data-text-variant="heading-md/medium" style="color: var(--text-strong);">Show Advanced Voice Settings</h1></div><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Automatic Gain Control, Advanced Voice Activity, Bypass System Audio Input Processing, and more</div></div><div class="baseControlItemTrailingElements_dbfbe0"><button id="react-aria3688859976-«r5ha»" class="triggerButton__64c86" data-rac="" type="button" tabindex="0" data-react-aria-pressable="true" aria-expanded="false" aria-controls="react-aria3688859976-«r5hb»" slot="trigger"><svg class="icon__64c86 iconClosed__64c86" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--icon-strong)" d="M3.3 15.7a1 1 0 0 0 1.4 0L12 8.42l7.3 7.3a1 1 0 0 0 1.4-1.42l-8-8a1 1 0 0 0-1.4 0l-8 8a1 1 0 0 0 0 1.42Z" class=""></path></svg></button></div></div><div class="panel__64c86" data-rac="" id="react-aria3688859976-«r5hb»" role="group" aria-labelledby="react-aria3688859976-«r5ha»" aria-hidden="true" hidden="until-found" style="--disclosure-panel-width: 0px; --disclosure-panel-height: 0px;"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-16); padding-top: var(--space-16);"><div data-debug-key="voice_automatic_gain_control_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5he»" for="«r5hf»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Automatic Gain Control</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5hh»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Automatically adjust microphone volume to keep it clear and consistent.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5hf»" aria-describedby="«r5hh»" aria-labelledby="«r5he»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div data-debug-key="advanced_voice_activity_processing_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="true"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5hj»" for="«r5hk»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Advanced Voice Activity</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5hm»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Turning this off may help if your voice is not being detected by the automatic input sensitivity.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278" data-disabled="true"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 0.5; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5hk»" aria-describedby="«r5hm»" aria-labelledby="«r5hj»" disabled="" data-react-aria-pressable="true" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div data-debug-key="voice_bypass_system_input_processing_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ho»" for="«r5hp»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Bypass System Audio Input Processing</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5hr»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">System audio processing may interfere with Discord's audio processing</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5hp»" aria-describedby="«r5hr»" aria-labelledby="«r5ho»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div data-debug-key="voice_silence_warning_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ht»" for="«r5hu»" data-text-variant="text-md/medium" style="color: var(--text-strong);">No Audio Detected Warning</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5i0»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Show a warning when Discord is not detecting audio from your mic</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5hu»" aria-describedby="«r5i0»" aria-labelledby="«r5ht»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="voice_switch_channel_alert_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5i2»" for="«r5i3»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Switch Voice Channel Warning</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5i5»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Show a confirmation prompt before switching to a different voice channel.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5i3»" aria-describedby="«r5i5»" aria-labelledby="«r5i2»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div data-debug-key="voice_global_attenuation_field_set" class="container__75920"><fieldset aria-describedby="«r5i7»"><legend class="text-md/semibold_cf4812 title__3c320 withSubtitle__3c320" data-text-variant="text-md/semibold" style="color: var(--text-strong);">Global Attenuation</legend><div class="text-sm/normal_cf4812 subtitle__3c320" id="«r5i7»" data-text-variant="text-sm/normal" style="color: var(--text-default);">Lower the volume of other applications by this percent when someone is speaking. +Set to 0% to completely disable global attenuation.</div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-top: var(--space-16);"><div data-debug-key="voice_global_attenuation_slider" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5i8»" for="«r5i9»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Attenuation Strength</label></div><div class="control__5a838"><div class="slider_a562c8" id="«r5i9»" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-disabled="false" aria-orientation="horizontal" aria-invalid="false" role="slider" tabindex="0" style="--grabber-size: 16px; --bar-size: 4px;"><div class="track_a562c8"></div><div class="bar_a562c8"><div class="barFill_a562c8" style="width: 0%;"></div></div><div class="track_a562c8"><div class="grabber_a562c8" aria-describedby="«r5id»" style="left: 0%;"></div><span id="«r5id»" class="hiddenVisually_b18fe2">0%</span></div></div></div></div></div></div><div data-debug-key="voice_global_attenuation_for_self_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ie»" for="«r5if»" data-text-variant="text-md/medium" style="color: var(--text-strong);">When I speak</label></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5if»" aria-labelledby="«r5ie»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="voice_global_attenuation_for_others_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ij»" for="«r5ik»" data-text-variant="text-md/medium" style="color: var(--text-strong);">When others speak</label></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ik»" aria-labelledby="«r5ij»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div></div></fieldset></div><div data-debug-key="voice_quality_of_service_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5io»" for="«r5ip»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Quality Of Service</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5ir»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Hints to your router that the packets Discord is transmitting are high priority. +Some routers or internet service providers may misbehave when this is set.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ip»" aria-describedby="«r5ir»" aria-labelledby="«r5io»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div></div></div></div></div></div></div><div class="divider__1de9c divider__143d2 divider__6131a"></div><div data-debug-key="camera_category" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-left: var(--space-12); padding-right: var(--space-12); padding-bottom: var(--space-24);"><div class="headerTitle__450f6"><h1 class="heading-xl/normal_cf4812 defaultColor__5345c" data-text-variant="heading-xl/normal" style="color: var(--text-strong);">Camera</h1></div></div><div data-settings-category-key="camera_category" aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="camera_video_preview" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="cameraWrapper__11e1f"><button data-mana-component="button" role="button" class="button_a22cb0 md_a22cb0 primary_a22cb0 hasText_a22cb0" type="button"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><span class="lineClamp1__4bd52 text-md/medium_cf4812" data-text-variant="text-md/medium">Test Video</span></div></div></button></div></div></div><div data-debug-key="camera_preview_preference" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5iu»" for="«r5iv»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Always preview video</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5j1»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Pops up preview modal every time you turn on video</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5iv»" aria-describedby="«r5j1»" aria-labelledby="«r5iu»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="camera_selection_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5j4»" for="«r5j5»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Camera</label></div><div class="control__5a838"><div class="container__72c38 container_a16aea" data-full-width="false" role="button" id="«r5j5»" aria-describedby="«r5j6»" aria-invalid="false" aria-busy="false" aria-disabled="false" aria-expanded="false" data-size="md" data-variant="filled" aria-haspopup="listbox" tabindex="0"><div class="wrapper__72c38 select_a16aea" data-disabled="false"><div class="defaultColor__4bd52 text-md/medium_cf4812 value_a16aea" data-text-variant="text-md/medium"><div class="deviceContainer__12eef withIcon__12eef"><div class="deviceIcon__12eef"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="var(--icon-muted)" d="M15 13a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" class=""></path><path fill="var(--icon-muted)" fill-rule="evenodd" d="M7.17 4.66A3 3 0 0 1 9.85 3h4.3a3 3 0 0 1 2.68 1.66c.34.69.94 1.34 1.71 1.34H20a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3h1.46c.77 0 1.37-.65 1.71-1.34ZM17 13a5 5 0 1 1-10 0 5 5 0 0 1 10 0Z" clip-rule="evenodd" class=""></path></svg></div><div class="lineClamp2Plus__4bd52 text-md/medium_cf4812 deviceLabel__12eef" data-text-variant="text-md/medium" style="color: var(--text-default); -webkit-line-clamp: 2;">OBS Virtual Camera</div></div></div><div class="icons_a16aea"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z" class=""></path></svg></div></div></div><div class="helperTextContainer__5a838"><div class="text-xs/normal_cf4812" id="«r5j6»" data-text-variant="text-xs/normal" style="color: var(--text-subtle);">Looking for more camera options? <a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Check out your system camera settings.</a></div></div></div></div></div></div><div data-debug-key="camera_background_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div><div class="container__5a838" data-layout="vertical"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5j9»" for="«r5ja»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Video Background</label></div><div class="control__5a838"><div class="backgroundOptions__53965"><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><div class="backgroundOptionContent__53965"><svg class="backgroundIconOptionIcon__53965" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-2 0a9 9 0 0 1-14.62 7.03L19.03 6.38A8.96 8.96 0 0 1 21 12ZM4.97 17.62 17.62 4.97A9 9 0 0 0 4.97 17.62Z" clip-rule="evenodd" class=""></path></svg><div class="text-sm/normal_cf4812 backgroundOptionText__53965 overflowEllipsis__53965" data-text-variant="text-sm/normal">None</div></div></div></div><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><div class="backgroundOptionBlurBackground__53965 backgroundOptionBackgroundBlurred__53965 backgroundImageOption__53965"></div><div class="backgroundOptionContent__53965"><svg class="backgroundIconOptionIcon__53965" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M21.7 2.3a1 1 0 0 1 0 1.4l-4.92 4.93c-.12.12-.33.09-.41-.06-.23-.42-.52-.8-.85-1.13a.26.26 0 0 1-.01-.36l4.78-4.79a1 1 0 0 1 1.42 0ZM20 20.6c0 .26.36.45.55.27l1.16-1.16a1 1 0 0 0-1.42-1.42l-.4.41a.25.25 0 0 0-.07.24c.12.53.18 1.09.18 1.66ZM4 20.6c0 .2.02.4.05.6a.26.26 0 0 1-.07.23l-.27.28a1 1 0 0 1-1.42-1.42l1.35-1.34c.19-.19.53.01.48.27-.08.45-.12.91-.12 1.38ZM16.7 3.7l-2.58 2.6a.26.26 0 0 1-.28.05A4.99 4.99 0 0 0 12 6c-.15 0-.23-.18-.13-.29L15.3 2.3a1 1 0 1 1 1.42 1.42ZM7 11c0-.15-.18-.23-.29-.13L2.3 15.3a1 1 0 1 0 1.42 1.42l3.58-3.6c.08-.06.1-.17.06-.27A4.99 4.99 0 0 1 7 11ZM18.23 15.36c-.1.1-.1.24-.02.35.32.37.6.77.83 1.2.09.14.29.18.41.05l2.26-2.25a1 1 0 0 0-1.42-1.42l-2.06 2.07ZM21.7 9.7l-4.62 4.64a.26.26 0 0 1-.33.03l-.45-.3a.27.27 0 0 1-.09-.37c.38-.6.64-1.27.74-2 0-.05.03-.1.07-.14L20.3 8.3a1 1 0 1 1 1.42 1.42ZM11.7 2.3a1 1 0 0 1 0 1.4l-8 8a1 1 0 0 1-1.4-1.4l8-8a1 1 0 0 1 1.4 0ZM6.7 3.7a1 1 0 0 0-1.4-1.4l-3 3a1 1 0 0 0 1.4 1.4l3-3ZM15 11a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6 20.6c0-3.1 2.5-5.6 5.6-5.6h.8c3.1 0 5.6 2.5 5.6 5.6 0 .77-.63 1.4-1.4 1.4a.17.17 0 0 1-.16-.12c-.19-.7-.44-1.36-.68-1.89-.11-.24-.43-.15-.4.12l.08.8a1 1 0 0 1-1 1.09H9.55a1 1 0 0 1-.99-1.1l.08-.79c.03-.27-.29-.36-.4-.12-.24.53-.5 1.19-.68 1.89a.17.17 0 0 1-.16.12A1.4 1.4 0 0 1 6 20.6Z" class=""></path></svg><div class="text-sm/normal_cf4812 backgroundOptionText__53965 overflowEllipsis__53965" data-text-variant="text-sm/normal">Blur</div></div></div></div><div class="newBackgroundTooltipContainer__53965" aria-describedby="«r5je»"><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><div class="backgroundCustomInlineUpsellBackground__53965 backgroundImageOption__53965"></div><div class="backgroundCustomInlineUpsellBackgroundDarkener__53965"></div><div class="backgroundOptionContent__53965"><svg class="backgroundIconOptionIcon__53965" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M2 5a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v8.67c0 .12-.34.17-.39.06A2.87 2.87 0 0 0 19 12a3 3 0 0 0-2.7 1.7c-.1.18-.36.22-.48.06l-.47-.63a2 2 0 0 0-3.2 0L9.93 16.1l-.5-.64a1.5 1.5 0 0 0-2.35 0l-1.86 2.32A.75.75 0 0 0 5.81 19h5.69c.28 0 .5.23.54.5.17.95.81 1.68 1.69 2.11.11.06.06.39-.06.39H5a3 3 0 0 1-3-3V5Zm8.2.98c.23-.91-.88-1.55-1.55-.9a.93.93 0 0 1-1.3 0c-.67-.65-1.78-.01-1.55.9a.93.93 0 0 1-.65 1.12c-.9.26-.9 1.54 0 1.8.48.14.77.63.65 1.12-.23.91.88 1.55 1.55.9a.93.93 0 0 1 1.3 0c.67.65 1.78.01 1.55-.9a.93.93 0 0 1 .65-1.12c.9-.26.9-1.54 0-1.8a.93.93 0 0 1-.65-1.12Z" clip-rule="evenodd" class=""></path><path fill="currentColor" d="M19 14a1 1 0 0 1 1 1v3h3a1 1 0 0 1 0 2h-3v3a1 1 0 0 1-2 0v-3h-3a1 1 0 1 1 0-2h3v-3a1 1 0 0 1 1-1Z" class=""></path></svg><div class="text-sm/normal_cf4812 backgroundOptionText__53965 overflowEllipsis__53965" data-text-variant="text-sm/normal"><div class="backgroundCustomInlineUpsell__53965"><svg class="backgroundCustomInlineUpsellIcon__53965" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M16.23 12c0 1.29-.95 2.25-2.22 2.25A2.18 2.18 0 0 1 11.8 12c0-1.29.95-2.25 2.22-2.25 1.27 0 2.22.96 2.22 2.25ZM23 12c0 5.01-4 9-8.99 9a8.93 8.93 0 0 1-8.75-6.9H3.34l-.9-4.2H5.3c.26-.96.68-1.89 1.21-2.7H1.89L1 3h12.74C19.13 3 23 6.99 23 12Zm-4.26 0c0-2.67-2.1-4.8-4.73-4.8A4.74 4.74 0 0 0 9.28 12c0 2.67 2.1 4.8 4.73 4.8a4.74 4.74 0 0 0 4.73-4.8Z" class=""></path></svg><div class="overflowEllipsis__53965">Custom</div></div></div></div></div></div></div><span id="«r5je»" class="hiddenVisually_b18fe2">Upload your own custom backgrounds with Nitro.</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><video class="backgroundImageOption__53965" src="https://cdn.discordapp.com/assets/content/0f96e9ef2cf5c9d2eef831bd1e4155973a608f9662cb9a2bf91a2523b918c803.mp4" loop=""></video></div><div class="playIcon__53965"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9.25 3.35C7.87 2.45 6 3.38 6 4.96v14.08c0 1.58 1.87 2.5 3.25 1.61l10.85-7.04a1.9 1.9 0 0 0 0-3.22L9.25 3.35Z" class=""></path></svg></div></div><span id="«r5jf»" class="hiddenVisually_b18fe2">Capernite Day</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><video class="backgroundImageOption__53965" src="https://cdn.discordapp.com/assets/content/52f91129995158682c465310f61e64cd61fbf227f0dc6b43313c5e8226818661.mp4" loop=""></video></div><div class="playIcon__53965"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9.25 3.35C7.87 2.45 6 3.38 6 4.96v14.08c0 1.58 1.87 2.5 3.25 1.61l10.85-7.04a1.9 1.9 0 0 0 0-3.22L9.25 3.35Z" class=""></path></svg></div></div><span id="«r5jg»" class="hiddenVisually_b18fe2">Capernite Night</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><video class="backgroundImageOption__53965" src="https://cdn.discordapp.com/assets/content/67802e4ea1da5f87f7610d4143626979e6337d37d4e16c1eff2d45276dd70a89.mp4" loop=""></video></div><div class="playIcon__53965"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9.25 3.35C7.87 2.45 6 3.38 6 4.96v14.08c0 1.58 1.87 2.5 3.25 1.61l10.85-7.04a1.9 1.9 0 0 0 0-3.22L9.25 3.35Z" class=""></path></svg></div><div class="newTextBadge__53965 textBadge__2b1f5 base__2b1f5 eyebrow__2b1f5 baseShapeRound__2b1f5" style="background-color: var(--badge-notification-background);">new</div></div><span id="«r5jh»" class="hiddenVisually_b18fe2">Hacker Den</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><video class="backgroundImageOption__53965" src="https://cdn.discordapp.com/assets/content/39743155a132962e7f660f16833725034975458ec167d30d9c6971f93b439422.mp4" loop=""></video></div><div class="playIcon__53965"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9.25 3.35C7.87 2.45 6 3.38 6 4.96v14.08c0 1.58 1.87 2.5 3.25 1.61l10.85-7.04a1.9 1.9 0 0 0 0-3.22L9.25 3.35Z" class=""></path></svg></div></div><span id="«r5ji»" class="hiddenVisually_b18fe2">Wumpice</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><div class="backgroundImageOption__53965" style="background-image: url("https://cdn.discordapp.com/assets/content/e321ddf99270fad41d4c5b21de89a82984d8469c892a3f9a924f5996764d33b4.png");"></div></div></div><span id="«r5jj»" class="hiddenVisually_b18fe2">Cybercity</span><div class="backgroundOption__53965 backgroundOptionSelected__53965" role="button" tabindex="0"><div class="backgroundOptionRing__53965"></div><div class="backgroundOptionInner__53965"><div class="backgroundImageOption__53965" style="background-image: url("https://cdn.discordapp.com/assets/content/435961b527cedf836cfdfb34cf7c4c7353b248f93be15c30a9a3b778b7d20c76.png");"></div></div></div><span id="«r5jk»" class="hiddenVisually_b18fe2">Discord the Movie</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><div class="backgroundImageOption__53965" style="background-image: url("https://cdn.discordapp.com/assets/content/cc4247f477caba753bfda3b66dd480a417c59ccd741b3809fedbfc3c8bdd49fd.png");"></div></div></div><span id="«r5jl»" class="hiddenVisually_b18fe2">Wumpus Vacation</span><div class="backgroundOption__53965" role="button" tabindex="0"><div class="backgroundOptionInner__53965"><div class="backgroundImageOption__53965" style="background-image: url("https://cdn.discordapp.com/assets/content/857242c637737acc689e8e06fdde1ce120fc595fd65833f4f86944eaea46527e.png");"></div></div></div><span id="«r5jm»" class="hiddenVisually_b18fe2">Vaporwave</span></div></div></div></div></div></div></div></div><div class="divider__1de9c divider__143d2 divider__6131a"></div><div data-debug-key="streaming_category" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-left: var(--space-12); padding-right: var(--space-12); padding-bottom: var(--space-24);"><div class="headerTitle__450f6"><h1 class="heading-xl/normal_cf4812 defaultColor__5345c" data-text-variant="heading-xl/normal" style="color: var(--text-strong);">Streaming</h1></div></div><div data-settings-category-key="streaming_category" aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="streaming_show_stream_previews" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5jn»" for="«r5jo»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Show Stream Previews</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5jq»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Allows others to see a preview of your stream before they join.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5jo»" aria-describedby="«r5jq»" aria-labelledby="«r5jn»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div data-debug-key="streaming_advanced_accordion" class="container__75920"><div class="react-aria-Disclosure" data-rac=""><div class="baseControlItem_dbfbe0 clickable_dbfbe0" role="button" tabindex="0" style="min-height: 72px;"><div class="baseControlItemContent_dbfbe0"><div class="baseControlItemTitle_dbfbe0"><h1 class="heading-md/medium_cf4812 defaultColor__5345c" data-text-variant="heading-md/medium" style="color: var(--text-strong);">Show Advanced Stream Settings</h1></div><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Stream Attenuation, Stream Attenuation Strength, and more</div></div><div class="baseControlItemTrailingElements_dbfbe0"><button id="react-aria3688859976-«r5ju»" class="triggerButton__64c86" data-rac="" type="button" tabindex="0" data-react-aria-pressable="true" aria-expanded="false" aria-controls="react-aria3688859976-«r5jv»" slot="trigger"><svg class="icon__64c86 iconClosed__64c86" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--icon-strong)" d="M3.3 15.7a1 1 0 0 0 1.4 0L12 8.42l7.3 7.3a1 1 0 0 0 1.4-1.42l-8-8a1 1 0 0 0-1.4 0l-8 8a1 1 0 0 0 0 1.42Z" class=""></path></svg></button></div></div><div class="panel__64c86" data-rac="" id="react-aria3688859976-«r5jv»" role="group" aria-labelledby="react-aria3688859976-«r5ju»" aria-hidden="true" hidden="until-found" style="--disclosure-panel-width: 0px; --disclosure-panel-height: 0px;"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-16); padding-top: var(--space-16);"><div data-debug-key="streaming_stream_attenuation" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5k2»" for="«r5k3»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Stream Attenuation</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5k5»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Automatically reduce stream volume when people are talking.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5k3»" aria-describedby="«r5k5»" aria-labelledby="«r5k2»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="streaming_stream_attenuation_strength" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5k7»" for="«r5k8»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Stream Attenuation Strength</label></div><div class="control__5a838"><div class="slider_a562c8" id="«r5k8»" aria-valuemin="1" aria-valuemax="100" aria-valuenow="50" aria-disabled="false" aria-orientation="horizontal" aria-invalid="false" role="slider" tabindex="0" style="--grabber-size: 16px; --bar-size: 4px;"><div class="track_a562c8"></div><div class="bar_a562c8"><div class="barFill_a562c8" style="width: 49.4949%;"></div></div><div class="track_a562c8"><div class="grabber_a562c8" aria-describedby="«r5kc»" style="left: 49.4949%;"></div><span id="«r5kc»" class="hiddenVisually_b18fe2">49%</span></div></div></div></div></div></div><div data-debug-key="streaming_advanced_screenshare" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5kd»" for="«r5ke»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Use our advanced technology to capture your screen</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5kg»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Our signed DLL is injected into the application to capture frames.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ke»" aria-describedby="«r5kg»" aria-labelledby="«r5kd»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div></div></div></div></div></div></div><div class="divider__1de9c divider__143d2 divider__6131a"></div><div data-debug-key="sounds_category" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-left: var(--space-12); padding-right: var(--space-12); padding-bottom: var(--space-24);"><div class="headerTitle__450f6"><h1 class="heading-xl/normal_cf4812 defaultColor__5345c" data-text-variant="heading-xl/normal" style="color: var(--text-strong);">Sounds</h1></div></div><div data-settings-category-key="sounds_category" aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="voice_and_video_sounds_list" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding: var(--space-0);"><div data-debug-key="sounds_list_item_deafen" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ki»" for="«r5kj»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Deafen</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5kl»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5kj»" aria-describedby="«r5kl»" aria-labelledby="«r5ki»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_undeafen" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5kn»" for="«r5ko»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Undeafen</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5kq»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ko»" aria-describedby="«r5kq»" aria-labelledby="«r5kn»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_mute" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ks»" for="«r5kt»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Mute</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5kv»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5kt»" aria-describedby="«r5kv»" aria-labelledby="«r5ks»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_unmute" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5l1»" for="«r5l2»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Unmute</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5l4»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5l2»" aria-describedby="«r5l4»" aria-labelledby="«r5l1»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="collapsibleContainer_a5d75d"><div class="divider__1de9c divider__143d2 hoverDivider_a5d75d"></div><div class="react-aria-Disclosure" data-rac=""><div class="baseControlItem_dbfbe0 clickable_dbfbe0" role="button" tabindex="0" style="min-height: 72px;"><div class="baseControlItemContent_dbfbe0"><div class="baseControlItemTitle_dbfbe0"><h1 class="heading-md/medium_cf4812 defaultColor__5345c" data-text-variant="heading-md/medium" style="color: var(--text-strong);">Show 18 more sounds</h1></div><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Camera On, Camera Off, Voice Disconnected, and more</div></div><div class="baseControlItemTrailingElements_dbfbe0"><button id="react-aria3688859976-«r5l7»" class="triggerButton__64c86" data-rac="" type="button" tabindex="0" data-react-aria-pressable="true" aria-expanded="false" aria-controls="react-aria3688859976-«r5l8»" slot="trigger"><svg class="icon__64c86 iconClosed__64c86" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--icon-strong)" d="M3.3 15.7a1 1 0 0 0 1.4 0L12 8.42l7.3 7.3a1 1 0 0 0 1.4-1.42l-8-8a1 1 0 0 0-1.4 0l-8 8a1 1 0 0 0 0 1.42Z" class=""></path></svg></button></div></div><div class="panel__64c86" data-rac="" id="react-aria3688859976-«r5l8»" role="group" aria-labelledby="react-aria3688859976-«r5l7»" aria-hidden="true" hidden="until-found" style="--disclosure-panel-width: 0px; --disclosure-panel-height: 0px;"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-16); padding-top: var(--space-16);"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding: var(--space-0);"><div data-debug-key="sounds_list_item_camera_on" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5lb»" for="«r5lc»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Camera On</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5le»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5lc»" aria-describedby="«r5le»" aria-labelledby="«r5lb»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_camera_off" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5lg»" for="«r5lh»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Camera Off</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5lj»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5lh»" aria-describedby="«r5lj»" aria-labelledby="«r5lg»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_disconnect" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5ll»" for="«r5lm»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Voice Disconnected</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5lo»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5lm»" aria-describedby="«r5lo»" aria-labelledby="«r5ll»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_ptt_start" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5lq»" for="«r5lr»" data-text-variant="text-md/medium" style="color: var(--text-strong);">PTT Activate</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5lt»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5lr»" aria-describedby="«r5lt»" aria-labelledby="«r5lq»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_ptt_stop" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5lv»" for="«r5m0»" data-text-variant="text-md/medium" style="color: var(--text-strong);">PTT Deactivate</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5m2»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5m0»" aria-describedby="«r5m2»" aria-labelledby="«r5lv»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_user_join" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5m4»" for="«r5m5»" data-text-variant="text-md/medium" style="color: var(--text-strong);">User Join</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5m7»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5m5»" aria-describedby="«r5m7»" aria-labelledby="«r5m4»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_user_leave" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5m9»" for="«r5ma»" data-text-variant="text-md/medium" style="color: var(--text-strong);">User Leave</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5mc»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ma»" aria-describedby="«r5mc»" aria-labelledby="«r5m9»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_user_moved" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5me»" for="«r5mf»" data-text-variant="text-md/medium" style="color: var(--text-strong);">User Moved</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5mh»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5mf»" aria-describedby="«r5mh»" aria-labelledby="«r5me»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_call_calling" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5mj»" for="«r5mk»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Outgoing Ring</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5mm»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5mk»" aria-describedby="«r5mm»" aria-labelledby="«r5mj»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_stream_started" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5mo»" for="«r5mp»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Stream Started</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5mr»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5mp»" aria-describedby="«r5mr»" aria-labelledby="«r5mo»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_stream_ended" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5mt»" for="«r5mu»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Stream Stopped</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5n0»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5mu»" aria-describedby="«r5n0»" aria-labelledby="«r5mt»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_stream_user_joined" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5n2»" for="«r5n3»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Viewer Join</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5n5»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5n3»" aria-describedby="«r5n5»" aria-labelledby="«r5n2»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_stream_user_left" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5n7»" for="«r5n8»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Viewer Leave</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5na»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5n8»" aria-describedby="«r5na»" aria-labelledby="«r5n7»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_activity_launch" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5nc»" for="«r5nd»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Activity Start</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5nf»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5nd»" aria-describedby="«r5nf»" aria-labelledby="«r5nc»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_activity_end" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5nh»" for="«r5ni»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Activity End</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5nk»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ni»" aria-describedby="«r5nk»" aria-labelledby="«r5nh»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_activity_user_join" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5nm»" for="«r5nn»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Activity User Join</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5np»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5nn»" aria-describedby="«r5np»" aria-labelledby="«r5nm»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_activity_user_left" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5nr»" for="«r5ns»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Activity User Leave</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5nu»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ns»" aria-describedby="«r5nu»" aria-labelledby="«r5nr»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div><div class="divider__1de9c divider__143d2"></div><div data-debug-key="sounds_list_item_reconnect" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5o0»" for="«r5o1»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Invited to Speak</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5o3»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);"><a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" role="button" tabindex="0">Preview Sound</a></div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5o1»" aria-describedby="«r5o3»" aria-labelledby="«r5o0»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div></div></div></div></div></div></div></div></div></div><div data-debug-key="voice_and_video_sounds_related_settings" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-top: var(--space-8);"><h1 class="heading-md/semibold_cf4812 defaultColor__5345c title__28f6b" data-text-variant="heading-md/semibold" style="color: var(--text-muted);">Related Settings</h1><div data-debug-key="voice_and_video_to_notification_sounds_navigator" class="container__75920"><div class="baseControlItem_dbfbe0 clickable_dbfbe0" role="button" tabindex="0"><div class="baseControlItemLeadingElement_dbfbe0"><div class="navigatorIcon__15430"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M9.7 2.89c.18-.07.32-.24.37-.43a2 2 0 0 1 3.86 0c.05.2.19.36.38.43A7 7 0 0 1 19 9.5v2.09c0 .12.05.24.13.33l1.1 1.22a3 3 0 0 1 .77 2.01v.28c0 .67-.34 1.29-.95 1.56-1.31.6-4 1.51-8.05 1.51-4.05 0-6.74-.91-8.05-1.5-.61-.28-.95-.9-.95-1.57v-.28a3 3 0 0 1 .77-2l1.1-1.23a.5.5 0 0 0 .13-.33V9.5a7 7 0 0 1 4.7-6.61ZM9.18 19.84A.16.16 0 0 0 9 20a3 3 0 1 0 6 0c0-.1-.09-.17-.18-.16a24.86 24.86 0 0 1-5.64 0Z" class=""></path></svg></div></div><div class="baseControlItemContent_dbfbe0"><div class="baseControlItemTitle_dbfbe0"><h1 class="heading-md/medium_cf4812 defaultColor__5345c" data-text-variant="heading-md/medium" style="color: var(--text-strong);">Notifications</h1></div><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Enable/disable sounds for new messages and incoming calls.</div></div><div class="baseControlItemTrailingElements_dbfbe0"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--interactive-icon-default)" d="M8.3 3.3a1 1 0 0 0 0 1.4l7.29 7.3-7.3 7.3a1 1 0 1 0 1.42 1.4l8-8a1 1 0 0 0 0-1.4l-8-8a1 1 0 0 0-1.42 0Z" class=""></path></svg></div></div></div></div></div></div></div><div class="divider__1de9c divider__143d2 divider__6131a"></div><div data-debug-key="soundboard_category" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-left: var(--space-12); padding-right: var(--space-12); padding-bottom: var(--space-24);"><div class="headerTitle__450f6"><h1 class="heading-xl/normal_cf4812 defaultColor__5345c" data-text-variant="heading-xl/normal" style="color: var(--text-strong);">Soundboard</h1></div></div><div data-settings-category-key="soundboard_category" aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="soundboard_volume_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="vertical" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5o5»" for="«r5o6»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Soundboard Volume</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5o8»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Control how loud sounds are for you personally. For more info, <a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" href="https://support.discord.com/hc/en-us/articles/12612888127767" rel="noreferrer noopener" target="_blank">click here</a>.</div></div><div class="control__5a838"><div class="slider_a562c8" id="«r5o6»" aria-valuemin="0" aria-valuemax="100" aria-valuenow="23.465037077260988" aria-disabled="false" aria-orientation="horizontal" aria-describedby="«r5o8»" aria-invalid="false" role="slider" tabindex="0" style="--grabber-size: 16px; --bar-size: 4px;"><div class="track_a562c8"></div><div class="bar_a562c8"><div class="barFill_a562c8" style="width: 23.465%;"></div></div><div class="track_a562c8"><div class="grabber_a562c8" aria-describedby="«r5oa»" style="left: 23.465%;"></div><span id="«r5oa»" class="hiddenVisually_b18fe2">23%</span></div></div></div></div></div></div><div data-debug-key="entrance_sounds_setting" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><fieldset class="fieldset__7fb92" aria-describedby="«r5ob»"><legend class="text-lg/medium_cf4812 legend__7fb92" data-text-variant="text-lg/medium" style="color: var(--text-strong);">Entrance Sounds</legend><div class="text-sm/normal_cf4812 description__7fb92" id="«r5ob»" data-text-variant="text-sm/normal" style="color: var(--text-default);">Choose a Soundboard sound to automatically play whenever you join a voice channel. Right-click on a voice channel to join without playing your entrance sound. <a class="anchor_edefb8 anchorUnderlineOnHover_edefb8" href="https://support.discord.com/hc/en-us/articles/12612888127767" rel="noreferrer noopener" target="_blank">Learn More</a>.</div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-24); padding: var(--space-0);"><div class="customizationSection_ace4f5 section__999b5 guildSelector__673eb hideDivider_ace4f5 withDivider_ace4f5"><h1 class="h5_b717a1 eyebrow_b717a1 title_ace4f5" data-migration-pending="true">Choose a Server</h1><div class="container__5a838" data-layout="vertical" style="--custom-field-horizontal-control-width: minmax(248px, auto);"><div class="control__5a838"><div class="container__72c38 selectFieldContainer__0edde fullWidth__0edde" data-full-width="true" data-mana-component="select-input-field" aria-busy="false"><div class="wrapper__72c38 selectField__0edde"><div class="selectFieldContent__0edde"><div class="comboBoxInputScroller__0edde"><div class="comboBoxInputContainer__0edde"><div class="singleSelectOption__0edde"><div class="listBoxItemContent__2e223 option__56a50 inInput__2e223" aria-hidden="true"><div class="lineClamp1__4bd52 text-md/normal_cf4812" data-text-variant="text-md/normal" style="color: currentcolor; grid-column: 1 / 2;">All Servers</div><div class="trailing__56a50"><div class=""><div class="pill__673eb"><svg class="pillIcon__673eb" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.92l4.28 4.68a1 1 0 0 0 .74.32H11a1 1 0 0 0 1-1V3ZM15.1 20.75c-.58.14-1.1-.33-1.1-.92v-.03c0-.5.37-.92.85-1.05a7 7 0 0 0 0-13.5A1.11 1.11 0 0 1 14 4.2v-.03c0-.6.52-1.06 1.1-.92a9 9 0 0 1 0 17.5Z" class=""></path><path fill="currentColor" d="M15.16 16.51c-.57.28-1.16-.2-1.16-.83v-.14c0-.43.28-.8.63-1.02a3 3 0 0 0 0-5.04c-.35-.23-.63-.6-.63-1.02v-.14c0-.63.59-1.1 1.16-.83a5 5 0 0 1 0 9.02Z" class=""></path></svg><div class="defaultColor__4bd52 text-xs/medium_cf4812 pillText__673eb" data-text-variant="text-xs/medium">Joined</div></div></div></div></div></div><input id="«r5of»" class="input__0f084 comboBoxInput__0edde hiddenVisually__0edde" placeholder="Search servers" role="combobox" aria-haspopup="listbox" aria-autocomplete="list" aria-busy="false" aria-controls="«r5od»" aria-expanded="false" aria-invalid="false" aria-labelledby="«r5oe»" value="All Servers"></div></div></div><span></span><button aria-hidden="true" tabindex="-1" class="chevronButton__0edde"><span class="chevronIcon__0edde"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M3.3 8.3a1 1 0 0 1 1.4 0l7.3 7.29 7.3-7.3a1 1 0 1 1 1.4 1.42l-8 8a1 1 0 0 1-1.4 0l-8-8a1 1 0 0 1 0-1.42Z" class=""></path></svg></span></button></div></div></div></div></div><div class="customizationSection_ace4f5"><h1 class="h5_b717a1 eyebrow_b717a1 title_ace4f5" data-migration-pending="true">Choose a sound <svg class="nitroWheel__673eb" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M16.23 12c0 1.29-.95 2.25-2.22 2.25A2.18 2.18 0 0 1 11.8 12c0-1.29.95-2.25 2.22-2.25 1.27 0 2.22.96 2.22 2.25ZM23 12c0 5.01-4 9-8.99 9a8.93 8.93 0 0 1-8.75-6.9H3.34l-.9-4.2H5.3c.26-.96.68-1.89 1.21-2.7H1.89L1 3h12.74C19.13 3 23 6.99 23 12Zm-4.26 0c0-2.67-2.1-4.8-4.73-4.8A4.74 4.74 0 0 0 9.28 12c0 2.67 2.1 4.8 4.73 4.8a4.74 4.74 0 0 0 4.73-4.8Z" class=""></path></svg></h1><div class="soundButtonSettingContainer__84bad"><div class="container__84bad"><div class="text-xs/medium_cf4812 soundText__84bad" data-text-variant="text-xs/medium" style="color: var(--text-strong);">Joined</div><div class="secondaryButton__9be63" aria-label="Preview Joined" aria-describedby="«r5oj»" role="button" tabindex="0"><svg class="secondaryIconActive__84bad secondaryIcon__84bad" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.92l4.28 4.68a1 1 0 0 0 .74.32H11a1 1 0 0 0 1-1V3ZM15.1 20.75c-.58.14-1.1-.33-1.1-.92v-.03c0-.5.37-.92.85-1.05a7 7 0 0 0 0-13.5A1.11 1.11 0 0 1 14 4.2v-.03c0-.6.52-1.06 1.1-.92a9 9 0 0 1 0 17.5Z" class=""></path><path fill="currentColor" d="M15.16 16.51c-.57.28-1.16-.2-1.16-.83v-.14c0-.43.28-.8.63-1.02a3 3 0 0 0 0-5.04c-.35-.23-.63-.6-.63-1.02v-.14c0-.63.59-1.1 1.16-.83a5 5 0 0 1 0 9.02Z" class=""></path></svg></div><span id="«r5oj»" class="hiddenVisually_b18fe2">Preview Joined</span></div><div class="container__84bad"><div class="secondaryButton__9be63" aria-label="Change sound" aria-describedby="«r5ok»" role="button" tabindex="0"><svg class="secondaryIcon__84bad" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" class=""></path></svg></div><span id="«r5ok»" class="hiddenVisually_b18fe2">Change sound</span><div class="secondaryButton__9be63" aria-label="Remove sound" aria-describedby="«r5ol»" role="button" tabindex="0"><svg class="secondaryIcon__84bad" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--red-400)" d="M14.25 1c.41 0 .75.34.75.75V3h5.25c.41 0 .75.34.75.75v.5c0 .41-.34.75-.75.75H3.75A.75.75 0 0 1 3 4.25v-.5c0-.41.34-.75.75-.75H9V1.75c0-.41.34-.75.75-.75h4.5Z" class=""></path><path fill="var(--red-400)" fill-rule="evenodd" d="M5.06 7a1 1 0 0 0-1 1.06l.76 12.13a3 3 0 0 0 3 2.81h8.36a3 3 0 0 0 3-2.81l.75-12.13a1 1 0 0 0-1-1.06H5.07ZM11 12a1 1 0 1 0-2 0v6a1 1 0 1 0 2 0v-6Zm3-1a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-6a1 1 0 0 1 1-1Z" clip-rule="evenodd" class=""></path></svg></div><span id="«r5ol»" class="hiddenVisually_b18fe2">Remove sound</span></div></div></div></div></fieldset></div></div></div></div><div class="divider__1de9c divider__143d2 divider__6131a"></div><div data-debug-key="voice_and_video_diagnostics_category" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-8); padding-left: var(--space-12); padding-right: var(--space-12); padding-bottom: var(--space-24);"><div class="headerTitle__450f6"><h1 class="heading-xl/normal_cf4812 defaultColor__5345c" data-text-variant="heading-xl/normal" style="color: var(--text-strong);">Advanced</h1></div></div><div data-settings-category-key="voice_and_video_diagnostics_category" aria-hidden="true" style="height: 1px;"></div><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-md); padding: var(--space-0);"><div data-debug-key="voice_and_video_diagnostics_accordion" class="container__75920"><div class="react-aria-Disclosure" data-rac=""><div class="baseControlItem_dbfbe0 clickable_dbfbe0" role="button" tabindex="0" style="min-height: 84px;"><div class="baseControlItemContent_dbfbe0"><div class="baseControlItemTitle_dbfbe0"><h1 class="heading-md/medium_cf4812 defaultColor__5345c" data-text-variant="heading-md/medium" style="color: var(--text-strong);">Show Diagnostic Settings</h1></div><div class="text-sm/normal_cf4812" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Settings that collect technical data for Discord Support to analyze, such as debug logging and audio recording</div></div><div class="baseControlItemTrailingElements_dbfbe0"><button id="react-aria3688859976-«r5oo»" class="triggerButton__64c86" data-rac="" type="button" tabindex="0" data-react-aria-pressable="true" aria-expanded="false" aria-controls="react-aria3688859976-«r5op»" slot="trigger"><svg class="icon__64c86 iconClosed__64c86" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="var(--icon-strong)" d="M3.3 15.7a1 1 0 0 0 1.4 0L12 8.42l7.3 7.3a1 1 0 0 0 1.4-1.42l-8-8a1 1 0 0 0-1.4 0l-8 8a1 1 0 0 0 0 1.42Z" class=""></path></svg></button></div></div><div class="panel__64c86" data-rac="" id="react-aria3688859976-«r5op»" role="group" aria-labelledby="react-aria3688859976-«r5oo»" aria-hidden="true" hidden="until-found" style="--disclosure-panel-width: 0px; --disclosure-panel-height: 0px;"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-16); padding-top: var(--space-16);"><div data-debug-key="voice_and_video_stream_info_overlay" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5os»" for="«r5ot»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Stream Info Overlay</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5ov»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Display stream information on video tiles on mouseover</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5ot»" aria-describedby="«r5ov»" aria-labelledby="«r5os»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="voice_and_video_audio_recording" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal" data-disabled="false"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5p1»" for="«r5p2»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Diagnostic Audio Recording</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5p4»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Diagnostic audio recording is used for analyzing audio problems. The last five minutes of voice is saved to voice module folder.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgba(0, 0, 0, 0.12); border: 1px solid rgba(151, 151, 159, 0.2);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 1px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5p2»" aria-describedby="«r5p4»" aria-labelledby="«r5p1»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox"></span></label></div></div></div></div><div data-debug-key="voice_and_video_debug_logging" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><fieldset><legend tag="legend" id="«r5p6»" class="hiddenVisually_b18fe2">Debug Logging Settings</legend><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-4); padding: var(--space-0);"><div class="container__5a838" data-layout="horizontal"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="true" id="«r5p7»" for="«r5p8»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Debug Logging</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5pa»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Saves debug logs to voice module folder that you can upload to Discord Support for troubleshooting.</div></div><div class="control__5a838"><label data-react-aria-pressable="true" class="container_a28278"><div class="switchIndicator_a28278" data-size="md" data-mana-component="switch" style="opacity: 1; background-color: rgb(88, 101, 242); border: 1px solid rgba(151, 151, 159, 0.12);"><svg class="thumb_a28278" viewBox="0 0 24 24" preserveAspectRatio="xMidYMin meet" aria-hidden="true" style="left: 24px;"><rect fill="rgba(255, 255, 255, 1)" x="4" y="4" width="16" height="16" rx="8"></rect></svg></div><span class="hiddenVisually_b18fe2"><input id="«r5p8»" aria-describedby="«r5pa»" aria-labelledby="«r5p7»" data-react-aria-pressable="true" tabindex="0" role="switch" type="checkbox" checked=""></span></label></div></div><div role="group" aria-labelledby="«r5p6»"><div data-align="stretch" data-justify="start" data-direction="horizontal" data-wrap="true" data-full-width="false" class="stack_dbd263" style="gap: var(--space-8); padding: var(--space-0);"><button data-mana-component="button" role="button" aria-busy="false" class="button_a22cb0 md_a22cb0 secondary_a22cb0 hasText_a22cb0" type="button" aria-label="Upload debug logs to Discord Support"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><span class="lineClamp1__4bd52 text-md/medium_cf4812" data-text-variant="text-md/medium">Upload Logs</span></div></div></button><button data-mana-component="button" role="button" class="button_a22cb0 md_a22cb0 secondary_a22cb0 hasText_a22cb0" type="button" aria-label="View debug logs folder"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><span class="lineClamp1__4bd52 text-md/medium_cf4812" data-text-variant="text-md/medium">Show Folder</span></div></div></button></div></div></div></fieldset></div></div></div></div></div></div><div data-debug-key="voice_and_video_reset_all_settings" class="container__75920"><div data-align="stretch" data-justify="start" data-direction="vertical" data-wrap="false" data-full-width="true" class="stack_dbd263" style="gap: var(--space-0); padding-left: var(--space-sm); padding-right: var(--space-sm); padding-top: var(--space-xs); padding-bottom: var(--space-xs);"><div class="container__5a838" data-layout="horizontal"><div class="labelContainer__5a838"><label class="text-md/medium_cf4812 label__5a838" aria-hidden="false" data-interactive="false" id="«r5pc»" for="«r5pd»" data-text-variant="text-md/medium" style="color: var(--text-strong);">Reset all Voice & Video settings</label><div class="text-sm/normal_cf4812 description__5a838" id="«r5pf»" data-text-variant="text-sm/normal" style="color: var(--text-subtle);">Return to the default set of Voice & Video settings.</div></div><div class="control__5a838"><button data-mana-component="button" role="button" aria-busy="false" class="button_a22cb0 md_a22cb0 critical-secondary_a22cb0 hasText_a22cb0" type="button"><div class="buttonChildrenWrapper_a22cb0"><div class="buttonChildren_a22cb0"><span class="lineClamp1__4bd52 text-md/medium_cf4812" data-text-variant="text-md/medium">Reset</span></div></div></button></div></div></div></div></div></div></div></div><div aria-hidden="true" style="position: absolute; pointer-events: none; min-height: 0px; min-width: 1px; flex: 0 0 auto; height: 0px;"></div></div><div></div></div></div></div></div></div></div> \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8fe08d6..45df557 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "discord-clone", "dependencies": { + "@convex-dev/presence": "^0.3.0", "convex": "^1.31.2", "livekit-server-sdk": "^2.15.0" } @@ -16,6 +17,27 @@ "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@convex-dev/presence": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@convex-dev/presence/-/presence-0.3.0.tgz", + "integrity": "sha512-adV+ao1L77u+egobyJabNwuai/0y/VgzMbqZiy+Q49JmX6fbSaiYg6FpFVACqPaq3giOfjrN2k+5mK2jTAUG0g==", + "license": "Apache-2.0", + "peerDependencies": { + "convex": "^1.24.8", + "expo-crypto": ">=14.1.0", + "react": "~18.3.1 || ^19.0.0", + "react-dom": "~18.3.1 || ^19.0.0", + "react-native": ">=0.79.0" + }, + "peerDependenciesMeta": { + "expo-crypto": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", @@ -608,6 +630,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", diff --git a/package.json b/package.json index 18ca597..76c2a5e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "install:all": "npm install && cd Frontend/Electron && npm install" }, "dependencies": { + "@convex-dev/presence": "^0.3.0", "convex": "^1.31.2", "livekit-server-sdk": "^2.15.0" }