mirror of
https://github.com/ancsemi/Haven.git
synced 2026-11-02 12:05:31 +00:00
Image Display Mode, Animated Profile Pictures and Animated Images in Chat sit together under one Image Display section instead of three scattered ones. Thanks to @birdcrazy.
4312 lines
341 KiB
HTML
4312 lines
341 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" data-haven-theme-api="1">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content">
|
||
<meta name="theme-color" content="#1a1a2e">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<link rel="manifest" href="/manifest.webmanifest">
|
||
<title>Haven</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cG9seWdvbiBwb2ludHM9IjUwLDMgOTMsMjggOTMsNzIgNTAsOTcgNyw3MiA3LDI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM2YjRmZGIiIHN0cm9rZS13aWR0aD0iOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjx0ZXh0IHg9IjUwIiB5PSI2MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsLEhlbHZldGljYSxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iYm9sZCIgZm9udC1zaXplPSIzOCIgZmlsbD0iIzZiNGZkYiIgb3BhY2l0eT0iMC45NSI+SDwvdGV4dD48L3N2Zz4=">
|
||
<link rel="stylesheet" href="/css/style.css?v=3.52.0">
|
||
<script src="/js/theme-compat.js?v=4.1.1"></script>
|
||
<script src="/js/theme-init.js?v=4.1.2"></script>
|
||
</head>
|
||
<body data-haven-page="app">
|
||
|
||
<div id="app" data-haven-region="app-shell">
|
||
<div id="app-body" data-haven-region="workspace">
|
||
|
||
<!-- ─── Server Bar (far left — like Discord) ─────── -->
|
||
<nav class="server-bar" id="server-bar" data-haven-region="server-rail">
|
||
<div class="server-icon home active" id="home-server" data-i18n-title="app.sidebar.this_server" title="This Server">
|
||
<span class="server-icon-text">⬡</span>
|
||
<span class="server-status-dot online"></span>
|
||
</div>
|
||
<div class="server-separator"></div>
|
||
<div id="server-list" data-haven-region="server-list"></div>
|
||
<button class="server-icon add-server" id="add-server-btn" data-i18n-title="app.sidebar.add_server" title="Add Server">
|
||
<span class="server-icon-text">+</span>
|
||
</button>
|
||
<button class="server-icon manage-servers" id="manage-servers-btn" data-i18n-title="app.sidebar.manage_servers" title="Manage Servers">
|
||
<span class="server-icon-text">⚙</span>
|
||
</button>
|
||
<button class="server-icon sync-servers" id="sync-servers-btn" data-i18n-title="app.sidebar.sync_servers" title="Sync Server List">
|
||
<span class="server-icon-text">⟳</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<!-- ─── Left Sidebar ────────────────────────────── -->
|
||
<aside class="sidebar" data-haven-region="navigation-sidebar">
|
||
<div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
|
||
<div class="sidebar-header">
|
||
<div class="brand">
|
||
<span class="logo-sm">⬡</span>
|
||
<span class="brand-text">HAVEN</span>
|
||
</div>
|
||
<button id="mobile-sidebar-close" class="mobile-panel-close" data-i18n-title="modals.common.close" data-i18n-aria-label="app.actions.close_sidebar" title="Close" aria-label="Close sidebar">×</button>
|
||
<div class="user-bar" data-haven-region="account">
|
||
<span class="led on" id="connection-led" data-i18n-title="app.status.server_connection" title="Server connection"></span>
|
||
<div class="user-names">
|
||
<span class="current-user" id="current-user"></span>
|
||
<span class="login-name" id="login-name" data-i18n-title="app.profile.login_username" title="Your login username"></span>
|
||
</div>
|
||
<button id="rename-btn" class="icon-btn small" data-i18n-title="app.actions.change_display_name" title="Change display name">✏️</button>
|
||
<button id="logout-btn" class="icon-btn" data-i18n-title="app.actions.logout" title="Logout">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||
<polyline points="16 17 21 12 16 7"/>
|
||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-mod-container" id="sidebar-mod-container" data-haven-region="sidebar-content">
|
||
<!-- Mobile server bubbles (visible only on mobile) -->
|
||
<div class="sidebar-section mobile-servers-section" id="mobile-servers-section">
|
||
<h5 class="section-label mobile-servers-toggle" id="mobile-servers-toggle" style="cursor:pointer;user-select:none">
|
||
<span class="collapse-toggle-arrow" id="mobile-servers-arrow">▾</span>
|
||
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.servers">Servers</span>
|
||
</h5>
|
||
<div class="mobile-servers-row" id="mobile-servers-row">
|
||
<div class="mobile-servers-scroll" id="mobile-servers-scroll"></div>
|
||
<button class="mobile-srv-add-btn" id="mobile-srv-add-btn" data-i18n-title="app.sidebar.add_server" title="Add Server">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section" data-mod-id="join" data-haven-region="join-channel">
|
||
<h5 class="section-label collapsible-section-toggle" id="join-section-toggle" style="cursor:pointer;user-select:none">
|
||
<span class="collapse-toggle-arrow" id="join-section-arrow">▾</span>
|
||
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.join_channel">Join a Channel</span>
|
||
</h5>
|
||
<div class="collapsible-section-body" id="join-section-body">
|
||
<div class="input-row">
|
||
<input type="text" id="channel-code-input"
|
||
data-i18n-placeholder="app.sidebar.join_placeholder" placeholder="Enter code..." maxlength="8" spellcheck="false">
|
||
<button id="join-channel-btn" class="btn-sm btn-accent" data-i18n="app.sidebar.join_btn">Join</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section" id="admin-controls" data-mod-id="create" data-haven-region="create-channel" style="display:none">
|
||
<h5 class="section-label collapsible-section-toggle" id="create-section-toggle" style="cursor:pointer;user-select:none">
|
||
<span class="collapse-toggle-arrow" id="create-section-arrow">▾</span>
|
||
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.create_channel">Create Channel</span>
|
||
</h5>
|
||
<div class="collapsible-section-body" id="create-section-body">
|
||
<div class="input-row">
|
||
<input type="text" id="new-channel-name"
|
||
data-i18n-placeholder="app.sidebar.channel_name_placeholder" placeholder="Channel name..." maxlength="50">
|
||
<button id="create-channel-btn" class="btn-sm btn-accent">+</button>
|
||
</div>
|
||
<label class="checkbox-row" data-i18n-title="app.sidebar.private_hint" title="Invite-only" style="margin:6px 0 2px;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
|
||
<input type="checkbox" id="new-channel-private">
|
||
<span>🔒 <span data-i18n="app.sidebar.private_channel">Private</span></span>
|
||
</label>
|
||
<label class="checkbox-row" data-i18n-title="app.sidebar.temporary_hint" title="Auto-deletes when empty" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
|
||
<input type="checkbox" id="new-channel-temporary">
|
||
<span>⏱️ <span data-i18n="app.sidebar.temporary_channel">Temporary</span></span>
|
||
</label>
|
||
<label class="checkbox-row" data-i18n-title="app.sidebar.forum_hint" title="Each message starts a topic; replies go in its thread" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
|
||
<input type="checkbox" id="new-channel-forum">
|
||
<span>🗂️ <span data-i18n="app.sidebar.forum_channel">Forum</span></span>
|
||
</label>
|
||
<label class="checkbox-row" id="new-channel-add-all-row" data-i18n-title="app.sidebar.add_all_hint" title="Everyone joins automatically" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
|
||
<input type="checkbox" id="new-channel-add-all">
|
||
<span>👥 <span data-i18n="app.sidebar.add_all_members">Add all members</span></span>
|
||
</label>
|
||
<div id="temp-channel-duration-row" style="display:none;margin:4px 0 2px;padding-left:22px">
|
||
<div class="input-row" style="gap:6px;align-items:center">
|
||
<input type="number" id="new-channel-duration" min="1" max="720" value="24" style="width:60px;font-size:0.8rem" data-i18n-placeholder="app.sidebar.hours" placeholder="Hours">
|
||
<span style="font-size:0.75rem;opacity:0.6" data-i18n="app.sidebar.hours">hours</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-split" id="sidebar-split" data-mod-id="channels">
|
||
<div class="sidebar-section channel-section" id="channels-pane" data-haven-region="channels">
|
||
<h5 class="section-label channels-toggle" id="channels-toggle" style="cursor:pointer;user-select:none"><span class="channels-toggle-arrow" id="channels-toggle-arrow">▾</span><span class="section-label-text" style="flex:1" data-i18n="app.sidebar.channels">Channels</span><button class="icon-btn small" id="sub-channel-panel-btn" data-i18n-title="app.actions.sub_channel_subscriptions" title="Sub-channel subscriptions" style="font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2;display:none">📡</button><button class="icon-btn small" id="organize-channels-btn" data-i18n-title="app.actions.organize_channels" title="Organize channels" style="display:none;font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2">📋</button></h5>
|
||
<div class="channel-list" id="channel-list"></div>
|
||
</div>
|
||
<div class="sidebar-split-handle" id="sidebar-split-handle" data-i18n-title="app.actions.drag_to_resize" title="Drag to resize"></div>
|
||
<div class="sidebar-section dm-section-pane" id="dm-pane" data-haven-region="direct-messages">
|
||
<h5 class="section-label dm-section-label dm-toggle" id="dm-toggle-header" style="cursor:pointer;user-select:none"><span class="dm-toggle-arrow" id="dm-toggle-arrow">▾</span><span class="section-label-text" style="flex:1" data-i18n="app.sidebar.direct_messages">Direct Messages</span><span style="display:flex;align-items:center;gap:4px;position:relative;z-index:2"><span class="dm-unread-count" id="dm-unread-badge" style="display:none"></span><button class="icon-btn small" id="organize-dms-btn" data-i18n-title="app.actions.organize_dms" title="Organize DMs" style="font-size:0.85rem;padding:2px 6px;opacity:0.6">📋</button></span></h5>
|
||
<div class="dm-list-scroll" id="dm-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pinned bottom area (never pushed off screen) -->
|
||
<div class="sidebar-bottom" data-haven-region="sidebar-footer">
|
||
<!-- Theme popup (floats above bottom bar) -->
|
||
<div id="theme-popup" class="theme-popup" style="display:none">
|
||
<div class="theme-popup-header">
|
||
<span class="theme-popup-title" data-i18n="app.theme.title">Theme</span>
|
||
<button id="theme-popup-close" class="icon-btn small" data-i18n-title="app.theme.close" title="Close">×</button>
|
||
</div>
|
||
<div class="theme-selector" id="theme-selector" data-haven-region="theme-picker">
|
||
<button class="theme-btn active" data-theme="haven" title="Haven"><span class="theme-icon">⬡</span></button>
|
||
<button class="theme-btn" data-theme="discord" title="Discord"><span class="theme-icon">🎮</span></button>
|
||
<button class="theme-btn" data-theme="matrix" title="Matrix"><span class="theme-icon">Ⅿ</span></button>
|
||
<button class="theme-btn" data-theme="tron" title="Tron"><span class="theme-icon">◈</span></button>
|
||
<button class="theme-btn" data-theme="halo" title="HALO"><span class="theme-icon">⌁</span></button>
|
||
<button class="theme-btn" data-theme="lotr" title="LoTR"><span class="theme-icon">⚜</span></button>
|
||
<button class="theme-btn" data-theme="cyberpunk" title="Cyberpunk"><span class="theme-icon">⚡</span></button>
|
||
<button class="theme-btn" data-theme="nord" title="Nord"><span class="theme-icon">❄</span></button>
|
||
<button class="theme-btn" data-theme="dracula" title="Dracula"><span class="theme-icon">🧛</span></button>
|
||
<button class="theme-btn" data-theme="bloodborne" title="Bloodborne"><span class="theme-icon">🩸</span></button>
|
||
<button class="theme-btn" data-theme="darksouls" title="Dark Souls"><span class="theme-icon">🔥</span></button>
|
||
<button class="theme-btn" data-theme="eldenring" title="Elden Ring"><span class="theme-icon">💍</span></button>
|
||
<button class="theme-btn" data-theme="ice" title="Ice"><span class="theme-icon">🧊</span></button>
|
||
<button class="theme-btn" data-theme="abyss" title="Abyss"><span class="theme-icon">🌊</span></button>
|
||
<button class="theme-btn" data-theme="minecraft" title="Minecraft"><span class="theme-icon">⛏️</span></button>
|
||
<button class="theme-btn" data-theme="ffx" title="Final Fantasy X"><span class="theme-icon">⚔️</span></button>
|
||
<button class="theme-btn" data-theme="zelda" title="Zelda"><span class="theme-icon">🗡️</span></button>
|
||
<button class="theme-btn" data-theme="fallout" title="Fallout (PIP Boy)"><span class="theme-icon">☢️</span></button>
|
||
<button class="theme-btn" data-theme="scripture" title="Scripture"><span class="theme-icon">✝️</span></button>
|
||
<button class="theme-btn" data-theme="chapel" title="Chapel"><span class="theme-icon">⛪</span></button>
|
||
<button class="theme-btn" data-theme="gospel" title="Gospel"><span class="theme-icon">🕊️</span></button>
|
||
<button class="theme-btn" data-theme="midnightpurple" title="Midnight Purple"><span class="theme-icon">🔮</span></button>
|
||
<button class="theme-btn" data-theme="crt" title="CRT"><span class="theme-icon">📺</span></button>
|
||
<button class="theme-btn" data-theme="win95" title="Windows 95"><span class="theme-icon">🪟</span></button>
|
||
<button class="theme-btn" data-theme="custom" title="Custom"><span class="theme-icon" id="custom-theme-swatch">🎨</span></button>
|
||
<button class="theme-btn" data-theme="rgb" title="RGB"><span class="theme-icon">🌈</span></button>
|
||
<button class="theme-btn" data-theme="daylight" title="Daylight (Light)"><span class="theme-icon">☀️</span></button>
|
||
<button class="theme-btn" data-theme="cloudy" title="Cloudy (Light)"><span class="theme-icon">☁️</span></button>
|
||
</div>
|
||
<!-- Effect overlay picker (mashup) -->
|
||
<div class="effect-overlay-section">
|
||
<div class="theme-popup-title" style="margin-top: 2px;" data-i18n="app.theme.effect_overlay">Effect Overlay</div>
|
||
<div class="effect-selector" id="effect-selector">
|
||
<button class="effect-btn active" data-effect="auto" data-i18n-title="app.theme.match_theme" title="Match theme">⟳</button>
|
||
<button class="effect-btn" data-effect="none" data-i18n-title="app.theme.no_effects" title="No effects">🚫</button>
|
||
<button class="effect-btn" data-effect="crt" data-i18n-title="app.theme.crt_scanlines" title="CRT Scanlines">📺</button>
|
||
<button class="effect-btn" data-effect="matrix" data-i18n-title="app.theme.matrix_rain" title="Matrix Rain">Ⅿ</button>
|
||
<button class="effect-btn" data-effect="matrixbars" data-i18n-title="app.theme.matrix_scan_lines" title="Matrix Scan Lines">▤</button>
|
||
<button class="effect-btn" data-effect="nord" data-i18n-title="app.theme.snowfall" title="Snowfall">❄</button>
|
||
<button class="effect-btn" data-effect="darksouls" data-i18n-title="app.theme.campfire" title="Campfire">🔥</button>
|
||
<button class="effect-btn" data-effect="eldenring" data-i18n-title="app.theme.golden_grace" title="Golden Grace">💍</button>
|
||
<button class="effect-btn" data-effect="bloodborne" data-i18n-title="app.theme.blood_vignette" title="Blood Vignette">🩸</button>
|
||
<button class="effect-btn" data-effect="fallout" data-i18n-title="app.theme.phosphor_glow" title="Phosphor Glow">☢️</button>
|
||
<button class="effect-btn" data-effect="ffx" data-i18n-title="app.theme.water_flow" title="Water Flow">⚔️</button>
|
||
<button class="effect-btn" data-effect="ice" data-i18n-title="app.theme.frost_shimmer" title="Frost Shimmer">🧊</button>
|
||
<button class="effect-btn" data-effect="cyberpunk" data-i18n-title="app.theme.glitch" title="Glitch">⚡</button>
|
||
<button class="effect-btn" data-effect="lotr" data-i18n-title="app.theme.candlelight" title="Candlelight">⚜</button>
|
||
<button class="effect-btn" data-effect="abyss" data-i18n-title="app.theme.ocean_depth" title="Ocean Depth">🌊</button>
|
||
<button class="effect-btn" data-effect="scripture" data-i18n-title="app.theme.cross_light" title="Cross Light">✝️</button>
|
||
<button class="effect-btn" data-effect="chapel" data-i18n-title="app.theme.stained_glass" title="Stained Glass">⛪</button>
|
||
<button class="effect-btn" data-effect="gospel" data-i18n-title="app.theme.divine_radiance" title="Divine Radiance">🕊️</button>
|
||
</div>
|
||
</div>
|
||
<!-- Custom theme triangle picker -->
|
||
<div id="custom-theme-editor" style="display:none">
|
||
<canvas id="custom-hue-bar" width="206" height="18"></canvas>
|
||
<canvas id="custom-triangle" width="206" height="180"></canvas>
|
||
</div>
|
||
<!-- RGB theme controls -->
|
||
<div id="rgb-theme-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.speed">Speed</span>
|
||
<input type="range" id="rgb-speed-slider" min="1" max="100" value="30" class="slider-sm rgb-slider">
|
||
</label>
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.vibrancy">Vibrancy</span>
|
||
<input type="range" id="rgb-vibrancy-slider" min="10" max="100" value="75" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- Effect speed slider (dynamic-effect themes) -->
|
||
<div id="effect-speed-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.effect_speed">Effect Speed</span>
|
||
<input type="range" id="effect-speed-slider" min="15" max="200" value="100" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- Sacred intensity slider (religious themes) -->
|
||
<div id="sacred-intensity-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.sacred_intensity">Sacred Intensity</span>
|
||
<input type="range" id="sacred-intensity-slider" min="20" max="250" value="100" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- Glitch frequency slider (cyberpunk) -->
|
||
<div id="glitch-freq-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.glitch_frequency">Glitch Frequency</span>
|
||
<input type="range" id="glitch-freq-slider" min="5" max="100" value="50" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- CRT effect sliders (vignette darkness + scanline intensity) -->
|
||
<div id="crt-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.crt_vignette">CRT Vignette</span>
|
||
<input type="range" id="crt-vignette-slider" min="0" max="100" value="50" class="slider-sm rgb-slider">
|
||
</label>
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.crt_scanlines">CRT Scanlines</span>
|
||
<input type="range" id="crt-scanline-slider" min="0" max="80" value="45" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom bar -->
|
||
<div id="voice-bar" class="voice-bar" style="display:none"></div>
|
||
<div class="sidebar-bottom-bar" data-haven-region="sidebar-actions">
|
||
<button id="theme-popup-toggle" class="sidebar-bottom-btn" data-i18n-title="app.theme.title" title="Themes">🎨</button>
|
||
<button class="sidebar-bottom-btn" id="activities-btn" data-i18n-title="modals.activities.title" title="Activities">🎮</button>
|
||
<button class="sidebar-bottom-btn" id="sidebar-members-btn" data-i18n-title="modals.all_members.title" title="All Members" style="display:none">👥</button>
|
||
<button id="mobile-settings-btn" class="sidebar-bottom-btn mobile-settings-btn" data-i18n-title="settings.title" title="Settings">⚙️</button>
|
||
<span style="flex:1"></span>
|
||
<button id="donors-btn" class="sidebar-bottom-btn donate-btn">❤️</button>
|
||
<button id="voice-mute-btn" class="sidebar-bottom-btn voice-header-btn" data-i18n-title="voice.mute" title="Mute" style="display:none">🎙️</button>
|
||
<button id="voice-deafen-btn" class="sidebar-bottom-btn voice-header-btn" data-i18n-title="voice.deafen" title="Deafen" style="display:none">🔊</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ─── Main Content ────────────────────────────── -->
|
||
<main class="main" data-haven-region="main">
|
||
<!-- Server Banner (always behind all content) -->
|
||
<div id="server-banner-display" class="server-banner-display" style="display:none">
|
||
<img id="server-banner-img" class="server-banner-img" src="" data-i18n-alt="media.server_banner_alt" alt="Server banner">
|
||
</div>
|
||
<header class="channel-header" data-haven-region="channel-header">
|
||
<button id="mobile-menu-btn" class="mobile-menu-btn" data-i18n-title="header.mobile_menu" data-i18n-aria-label="header.mobile_menu" title="Menu" aria-label="Menu">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
<div class="mobile-server-dropdown" id="mobile-server-dropdown">
|
||
<button class="mobile-server-btn" id="mobile-server-btn" data-i18n-title="header.mobile_servers" data-i18n-aria-label="header.mobile_servers" title="Servers" aria-label="Servers">⬡</button>
|
||
<div class="mobile-server-menu" id="mobile-server-menu">
|
||
<div class="mobile-server-menu-header" data-i18n="header.mobile_servers">Servers</div>
|
||
<div id="mobile-server-list"></div>
|
||
<button class="mobile-server-add" id="mobile-server-add-btn" data-i18n="header.mobile_add_server">+ Add Server</button>
|
||
</div>
|
||
</div>
|
||
<div class="channel-info">
|
||
<h3 id="channel-header-name" data-i18n="header.select_channel">Select a channel</h3>
|
||
</div>
|
||
<div class="header-actions-box" id="header-actions-box" style="display:none">
|
||
<button id="thread-mentions-pill" class="icon-btn small thread-mentions-pill" data-i18n-title="header.thread_mentions" title="Mentions in threads" style="display:none">🔔 <span id="thread-mentions-pill-count">0</span></button>
|
||
<span class="channel-code-tag" id="channel-code-display" data-i18n-title="header.channel_code" title="Channel code"></span>
|
||
<button id="copy-code-btn" class="icon-btn small" data-i18n-title="header.copy_code" title="Copy code">📋</button>
|
||
<button id="channel-code-settings-btn" class="icon-btn small" data-i18n-title="header.channel_code_settings" title="Channel code settings">⚙️</button>
|
||
<span class="header-actions-divider"></span>
|
||
<button id="search-toggle-btn" class="icon-btn small" data-i18n-title="header.search_messages" title="Search messages (Ctrl+F)">🔍</button>
|
||
<button id="pinned-toggle-btn" class="icon-btn small" data-i18n-title="header.pinned_messages" title="Pinned messages">📌</button>
|
||
<button id="gallery-toggle-btn" class="icon-btn small" data-i18n-title="header.media_gallery" title="View files & media">🖼</button>
|
||
<button id="threads-toggle-btn" class="icon-btn small" data-i18n-title="header.thread_list" title="View threads">🧵</button>
|
||
<button id="move-select-btn" class="icon-btn small" data-i18n-title="header.select_messages_to_move" title="Select messages to move" style="display:none">☰</button>
|
||
<div class="e2e-menu-wrapper" style="display:none" id="e2e-menu-wrapper">
|
||
<button id="e2e-menu-btn" class="icon-btn small" data-i18n-title="header.encryption_options" title="Encryption options">🔐</button>
|
||
<div class="e2e-dropdown" id="e2e-dropdown" style="display:none">
|
||
<button class="e2e-dropdown-item" id="e2e-verify-btn">🔑 <span data-i18n="header.verify_encryption">Verify Encryption</span></button>
|
||
<button class="e2e-dropdown-item" id="e2e-recover-btn">🔄 <span data-i18n="header.recover_encryption">Recover Keys from Backup</span></button>
|
||
<button class="e2e-dropdown-item e2e-danger" id="e2e-reset-btn">⚠️ <span data-i18n="header.reset_encryption">Reset Encryption Keys</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="search-container" id="search-container" style="display:none">
|
||
<input type="text" id="search-input" class="search-input" data-i18n-placeholder="header.search_placeholder" placeholder="Search... from:user in:#channel has:image" maxlength="200">
|
||
<button id="search-filter-btn" class="icon-btn small" data-i18n-title="header.filter_toggle" title="Filters">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||
</button>
|
||
<button id="search-close-btn" class="icon-btn small" data-i18n-title="header.search_close" title="Close search">×</button>
|
||
<!-- Filter picker popover — appears with the search box, appends
|
||
filter tokens client-side. Public search only. (phase 3) -->
|
||
<div id="search-filter-popover" class="search-filter-popover" style="display:none">
|
||
<div class="sfp-root" id="sfp-root">
|
||
<div class="sfp-recent" id="sfp-recent" style="display:none"></div>
|
||
<div class="sfp-chips">
|
||
<span class="sfp-label" data-i18n="header.filter_by">Filter by</span>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="from">from:</button>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="in">in:</button>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="has">has:</button>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="before">before:</button>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="after">after:</button>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="during">during:</button>
|
||
<button type="button" class="sfp-chip" data-sfp-filter="pinned">pinned</button>
|
||
</div>
|
||
<div class="sfp-sort" id="sfp-sort">
|
||
<span class="sfp-label" data-i18n="header.sort_by">Sort</span>
|
||
<button type="button" class="sfp-sort-btn" data-sort="newest" data-i18n="header.sort_newest">Newest</button>
|
||
<button type="button" class="sfp-sort-btn" data-sort="oldest" data-i18n="header.sort_oldest">Oldest</button>
|
||
<button type="button" class="sfp-sort-btn" data-sort="relevant" data-i18n="header.sort_relevant">Relevant</button>
|
||
</div>
|
||
</div>
|
||
<div class="sfp-picker" id="sfp-picker" style="display:none">
|
||
<div class="sfp-picker-head">
|
||
<button type="button" class="sfp-back" id="sfp-back" data-i18n-title="modals.common.back" title="Back">‹</button>
|
||
<input type="text" class="sfp-filter-input" id="sfp-filter-input" autocomplete="off" spellcheck="false" data-i18n-placeholder="header.filter_placeholder" placeholder="Filter...">
|
||
<div class="sfp-date-wrap" id="sfp-date-wrap" style="display:none">
|
||
<input type="date" class="sfp-date-input" id="sfp-date-input">
|
||
<button type="button" class="sfp-chip" id="sfp-date-apply" data-i18n="header.filter_apply">Apply</button>
|
||
</div>
|
||
</div>
|
||
<div class="sfp-list" id="sfp-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Update available banner -->
|
||
<a id="update-banner" class="update-banner" href="#" style="display:none" target="_blank" rel="noopener">
|
||
<span class="update-pulse"></span>
|
||
<span class="update-text" data-i18n="header.update_available">Update Available</span>
|
||
</a>
|
||
<!-- Low disk warning (admins only, #5505) -->
|
||
<span id="disk-low-banner" class="disk-low-banner" style="display:none" role="status">
|
||
<span class="disk-low-dot"></span>
|
||
<span class="disk-low-text" data-i18n="banners.disk_low">Server disk almost full</span>
|
||
</span>
|
||
<!-- Desktop app banner -->
|
||
<!-- The dismiss button is a SIBLING of the link, not inside it. A
|
||
<button> nested in an <a> is invalid HTML: the parser hoists it
|
||
out, so clicking the X activated the anchor and opened the
|
||
download page instead of dismissing. Same trap as the Android
|
||
banner below, which hit it with nested <button>s. -->
|
||
<span id="desktop-app-banner" class="desktop-app-banner" style="display:none">
|
||
<a class="desktop-app-link" href="https://ancsemi.github.io/Haven/#download" target="_blank" rel="noopener">
|
||
<span class="desktop-app-icon">🖥️</span>
|
||
<span class="desktop-app-text" data-i18n="header.get_desktop_app">Get the Desktop App</span>
|
||
</a>
|
||
<button type="button" class="desktop-app-dismiss" id="desktop-app-dismiss" data-i18n-title="modals.common.dismiss" data-i18n-aria-label="modals.common.dismiss" title="Dismiss" aria-label="Dismiss">×</button>
|
||
</span>
|
||
<!-- Android beta banner (must be <div>, not <button>; nesting
|
||
<button> inside <button> is invalid HTML and causes the
|
||
dismiss × to be hoisted out of its parent by the parser,
|
||
creating a phantom X in the channel header on mobile) -->
|
||
<div id="android-beta-banner" class="android-beta-banner" role="button" tabindex="0" style="display:none">
|
||
<span class="android-beta-icon">📱</span>
|
||
<span class="android-beta-text" data-i18n="header.android_beta">Android App</span>
|
||
<button class="android-beta-dismiss" id="android-beta-dismiss" data-i18n-title="modals.common.dismiss" data-i18n-aria-label="modals.common.dismiss" title="Dismiss" aria-label="Dismiss">×</button>
|
||
</div>
|
||
<!-- Spacer pushes voice controls to the right -->
|
||
<div style="flex:1"></div>
|
||
<!-- Right-side: dynamic indicators, then voice (always furthest right) -->
|
||
<div class="voice-controls">
|
||
<button id="mobile-users-btn" class="mobile-users-btn" data-i18n-title="modals.all_members.title" data-i18n-aria-label="modals.all_members.title" title="Members" aria-label="Members">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||
<circle cx="9" cy="7" r="4"/>
|
||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||
</svg>
|
||
</button>
|
||
<!-- Dynamic indicators (music, streams) inject here via JS -->
|
||
<button id="voice-join-btn" class="btn-voice" style="display:none"><span data-i18n="voice.join">🎤 Join Voice</span></button>
|
||
<div id="voice-active-indicator" class="voice-active-indicator" style="display:none">
|
||
<span class="voice-indicator-icon">🎤</span>
|
||
<span class="voice-indicator-text" data-i18n="header.voice_active">Voice Active</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Welcome Screen -->
|
||
<div id="no-channel-msg" class="welcome-screen" data-haven-region="welcome">
|
||
<div class="welcome-content">
|
||
<div class="welcome-icon">⬡</div>
|
||
<h2 data-i18n="welcome.title">Welcome to Haven</h2>
|
||
<p data-i18n="welcome.subtitle">Join a channel with a code from your friends,<br>
|
||
or create one if you're the admin.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Area -->
|
||
<div id="message-area" class="message-area" data-haven-region="message-area" style="display:none">
|
||
<!-- Webcam Grid -->
|
||
<div id="webcam-container" class="webcam-container" data-haven-region="webcams" style="display:none">
|
||
<div class="webcam-header">
|
||
<span id="webcam-label">📷 <span data-i18n="media.cameras">Cameras</span></span>
|
||
<div class="webcam-header-controls">
|
||
<div class="webcam-layout-picker" id="webcam-layout-picker" data-i18n-title="media.camera_layout" title="Camera layout">
|
||
<button class="stream-layout-btn" id="webcam-layout-btn" data-i18n-title="media.change_layout" title="Change layout">⊞</button>
|
||
<div class="stream-layout-menu" id="webcam-layout-menu">
|
||
<button class="stream-layout-opt active" data-layout="auto" data-i18n-title="media.layout_auto_title" title="Auto grid">⊞ <span data-i18n="media.layout_auto">Auto</span></button>
|
||
<button class="stream-layout-opt" data-layout="vertical" data-i18n-title="media.layout_stack_title" title="Vertical stack">▬ <span data-i18n="media.layout_stack">Stack</span></button>
|
||
<button class="stream-layout-opt" data-layout="side-by-side" data-i18n-title="media.layout_side_title" title="Side by side">◫ <span data-i18n="media.layout_side">Side</span></button>
|
||
<button class="stream-layout-opt" data-layout="grid-2x2" data-i18n-title="media.layout_2x2_title" title="2×2 Grid">⊟ <span data-i18n="media.layout_2x2">2×2</span></button>
|
||
</div>
|
||
</div>
|
||
<label class="stream-size-label" data-i18n-title="media.camera_size" title="Camera display size">
|
||
📷
|
||
<input type="range" id="webcam-size-slider" class="stream-size-slider" min="15" max="70" value="25">
|
||
</label>
|
||
<button id="webcam-collapse-btn" class="icon-btn small" data-i18n-title="media.minimize_cameras" title="Minimize cameras">─</button>
|
||
<button id="webcam-close-btn" class="icon-btn small" data-i18n-title="media.close_cameras" title="Close cameras">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="webcam-grid" class="webcam-grid"></div>
|
||
</div>
|
||
<!-- Screen Share Viewer (multi-stream) -->
|
||
<div id="screen-share-container" class="screen-share-container" data-haven-region="screen-shares" style="display:none">
|
||
<div class="screen-share-header">
|
||
<span id="screen-share-label">🖥️ <span data-i18n="media.streams">Streams</span></span>
|
||
<div class="screen-share-header-controls">
|
||
<div class="stream-layout-picker" id="stream-layout-picker" data-i18n-title="media.stream_layout" title="Stream layout">
|
||
<button class="stream-layout-btn" id="stream-layout-btn" data-i18n-title="media.change_layout" title="Change layout">⊞</button>
|
||
<div class="stream-layout-menu" id="stream-layout-menu">
|
||
<button class="stream-layout-opt active" data-layout="auto" data-i18n-title="media.layout_auto_title" title="Auto grid">⊞ <span data-i18n="media.layout_auto">Auto</span></button>
|
||
<button class="stream-layout-opt" data-layout="vertical" data-i18n-title="media.layout_stack_title" title="Vertical stack">▬ <span data-i18n="media.layout_stack">Stack</span></button>
|
||
<button class="stream-layout-opt" data-layout="side-by-side" data-i18n-title="media.layout_side_title" title="Side by side">◫ <span data-i18n="media.layout_side">Side</span></button>
|
||
<button class="stream-layout-opt" data-layout="grid-2x2" data-i18n-title="media.layout_2x2_title" title="2×2 Grid">⊟ <span data-i18n="media.layout_2x2">2×2</span></button>
|
||
</div>
|
||
</div>
|
||
<label class="stream-size-label" data-i18n-title="media.stream_size" title="Stream display size">
|
||
🖥️
|
||
<input type="range" id="stream-size-slider" class="stream-size-slider" min="20" max="90" value="50">
|
||
</label>
|
||
<button id="screen-share-minimize" class="icon-btn small" data-i18n-title="media.minimize_streams" title="Minimize streams">─</button>
|
||
<button id="screen-share-close" class="icon-btn small" data-i18n-title="media.close_streams" title="Close streams">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="screen-share-grid" class="screen-share-grid"></div>
|
||
</div>
|
||
<!-- Music Player Panel -->
|
||
<div id="music-panel" class="music-panel" data-haven-region="music-player" style="display:none">
|
||
<div class="music-panel-header">
|
||
<div class="music-panel-header-left">
|
||
<button id="music-popout-btn" class="icon-btn small" data-i18n-title="media.music_popout" title="Pop out player">⧉</button>
|
||
<div class="music-panel-copy">
|
||
<span id="music-panel-label">🎵 <span data-i18n="media.listening_together">Listening Together</span></span>
|
||
<span id="music-up-next" class="music-up-next" data-i18n="media.music_up_next_empty">Up next: Nothing queued</span>
|
||
</div>
|
||
</div>
|
||
<span id="music-activity-hint" class="music-activity-hint"></span>
|
||
<div class="music-panel-controls">
|
||
<button id="music-queue-btn" class="icon-btn small" data-i18n-title="media.music_queue" title="Queue">☰</button>
|
||
<button id="music-play-pause-btn" class="icon-btn small" data-i18n-title="media.music_play_pause" title="Play/Pause">⏸</button>
|
||
<button id="music-next-btn" class="icon-btn small" data-i18n-title="media.music_next" title="Next track" style="display:none">⏭</button>
|
||
<span id="music-time-current" class="music-time">0:00</span>
|
||
<input type="range" id="music-seek-slider" class="music-seek-slider" min="0" max="100" value="0" step="0.1" data-i18n-title="media.music_seek" title="Seek">
|
||
<span id="music-time-duration" class="music-time">0:00</span>
|
||
<button id="music-mute-btn" class="icon-btn small" data-i18n-title="media.music_mute" title="Mute music">🔊</button>
|
||
<input type="range" id="music-volume-slider" class="stream-vol-slider" min="0" max="100" value="80" data-i18n-title="media.music_volume" title="Music volume">
|
||
<button id="music-close-btn" class="icon-btn small" data-i18n-title="media.minimize" title="Minimize">─</button>
|
||
<button id="music-stop-btn" class="icon-btn small" data-i18n-title="media.music_stop" title="Close / stop music">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="music-embed-container" class="music-embed-container"></div>
|
||
</div>
|
||
<div id="pinned-panel" class="pinned-panel" data-haven-region="pinned-messages" style="display:none">
|
||
<div class="pinned-panel-header">
|
||
<span id="pinned-count">📌 <span data-i18n="header.pinned_count">Pinned Messages</span></span>
|
||
<div class="pinned-panel-actions">
|
||
<button id="pinned-fullscreen-btn" class="icon-btn small" data-i18n-title="media.open_fullscreen" title="Open fullscreen">⤢</button>
|
||
<button id="pinned-popup-btn" class="icon-btn small" data-i18n-title="media.popout_panel" title="Pop out to floating panel">⧉</button>
|
||
<button id="pinned-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
</div>
|
||
<div id="pinned-list" class="pinned-list"></div>
|
||
</div>
|
||
<div class="messages" id="messages" data-haven-region="message-list">
|
||
<!-- Floating "⋯" button for touch-device message actions fallback -->
|
||
<button id="msg-more-btn" data-i18n-aria-label="app.actions.message_actions" aria-label="Message actions">⋯</button>
|
||
</div>
|
||
<button id="jump-to-bottom" class="jump-to-bottom" data-i18n-title="app.actions.jump_to_bottom" title="Jump to bottom">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="6 9 12 15 18 9"/>
|
||
</svg>
|
||
</button>
|
||
<div class="typing-indicator" id="typing-indicator"></div>
|
||
<div id="reply-bar" class="reply-bar" style="display:none">
|
||
<span class="reply-bar-text" id="reply-preview-text"></span>
|
||
<button class="reply-bar-close" id="reply-close-btn" data-i18n-title="header.cancel_reply" title="Cancel reply">×</button>
|
||
</div>
|
||
<div id="image-queue-bar" class="image-queue-bar" style="display:none"></div>
|
||
<div id="upload-progress-bar" class="upload-progress-bar" style="display:none">
|
||
<div class="upload-progress-track"><div class="upload-progress-fill" id="upload-progress-fill"></div></div>
|
||
<span class="upload-progress-text" id="upload-progress-text" data-i18n="header.uploading">Uploading...</span>
|
||
<button class="upload-progress-cancel" id="upload-cancel-btn" data-i18n-title="common.cancel_upload" data-i18n-aria-label="common.cancel_upload" title="Cancel upload" aria-label="Cancel upload">×</button>
|
||
</div>
|
||
<div id="message-input-area" class="message-input-area" data-haven-region="composer">
|
||
<div class="pip-input-resizer" aria-hidden="true"></div>
|
||
<div class="input-actions-box">
|
||
<button id="upload-btn" class="btn-upload" data-i18n-title="media.upload_file_title" title="Upload File">
|
||
<svg class="upload-icon-default" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<path d="M21 15l-5-5L5 21"/>
|
||
</svg>
|
||
<span class="upload-icon-clippy">📎</span>
|
||
</button>
|
||
<span class="input-actions-divider"></span>
|
||
<button id="emoji-btn" class="btn-emoji" data-i18n-title="app.input_bar.emoji_btn" title="Emoji">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M8 14s1.5 2 4 2 4-2 4-2"/>
|
||
<circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"/>
|
||
<circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"/>
|
||
</svg>
|
||
</button>
|
||
<span class="input-actions-divider"></span>
|
||
<button id="gif-btn" class="btn-gif" data-i18n-title="app.input_bar.gif_btn" title="Search GIFs"><span data-i18n="app.input_bar.gif_label">GIF</span></button>
|
||
<span class="input-actions-divider"></span>
|
||
<button id="poll-btn" class="btn-poll" data-i18n-title="app.input_bar.poll_btn" title="Create Poll">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||
<line x1="2" y1="20" x2="22" y2="20"/>
|
||
</svg>
|
||
</button>
|
||
<span class="input-actions-divider" id="time-divider"></span>
|
||
<button id="time-btn" class="btn-poll btn-time" data-i18n-title="app.input_bar.time_btn" title="Insert timestamp">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="9"/>
|
||
<line x1="12" y1="12" x2="12" y2="7"/>
|
||
<line x1="12" y1="12" x2="15.5" y2="13.5"/>
|
||
</svg>
|
||
</button>
|
||
<span class="input-actions-divider" id="burn-divider" style="display:none"></span>
|
||
<button id="burn-btn" class="btn-burn" data-i18n-title="app.input_bar.burn_btn" title="Burn after read (DM only)" style="display:none">🔥</button>
|
||
</div>
|
||
<div id="emoji-picker" class="emoji-picker" style="display:none"></div>
|
||
<div id="gif-picker" class="gif-picker" style="display:none">
|
||
<div class="gif-picker-tabs">
|
||
<button type="button" class="gif-tab active" data-gif-tab="search" data-i18n="gifs.tab_search">Search</button>
|
||
<button type="button" class="gif-tab" data-gif-tab="favorites" data-i18n="gifs.tab_favorites">★ Favorites</button>
|
||
</div>
|
||
<div class="gif-picker-header">
|
||
<input type="text" id="gif-search-input" data-i18n-placeholder="header.gif_search_placeholder" placeholder="Search GIFs..." maxlength="100" autocomplete="off">
|
||
</div>
|
||
<div class="gif-picker-grid" id="gif-grid"></div>
|
||
<div class="gif-picker-footer" data-i18n="header.powered_by_giphy">Powered by GIPHY</div>
|
||
</div>
|
||
<div id="mention-dropdown" class="mention-dropdown" style="display:none"></div>
|
||
<div id="channel-dropdown" class="mention-dropdown" style="display:none"></div>
|
||
<div id="emoji-dropdown" class="emoji-dropdown" style="display:none"></div>
|
||
<div id="slash-dropdown" class="slash-dropdown" style="display:none"></div>
|
||
<div id="persona-dropdown" class="mention-dropdown" style="display:none"></div>
|
||
<div id="ferry-dropdown" class="mention-dropdown" style="display:none"></div>
|
||
<textarea id="message-input" data-i18n-placeholder="header.message_placeholder_commands" placeholder="Type a message... (/ for commands)"
|
||
rows="1" maxlength="2000"></textarea>
|
||
<button id="send-btn" class="btn-send" data-i18n-title="media.send_title" title="Send">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<input type="file" id="file-input" style="display:none">
|
||
</div>
|
||
</main>
|
||
|
||
<!-- ─── Soundboard Sidebar (twin to voice/users — sits directly LEFT of right-sidebar) ─── -->
|
||
<!-- Soundboard toggle arrow, fixed, sits at left edge of soundboard panel.
|
||
The note marks it apart from the members handle right above it, which
|
||
is otherwise the same little arrow in the same place. (#5534) -->
|
||
<button id="sb-sidebar-toggle-btn" class="sb-sidebar-toggle-btn" style="display:none" data-i18n-title="media.toggle_soundboard" title="Toggle soundboard panel"><span class="sb-toggle-mark" aria-hidden="true">♪</span><span class="sb-toggle-arrow">❮</span></button>
|
||
<aside id="sb-sidebar-panel" class="sb-sidebar-panel sb-hidden" data-haven-region="soundboard">
|
||
<div id="sb-sidebar-resize-handle" class="sb-sidebar-resize-handle"></div>
|
||
<div class="sb-sidebar-header">🎵 <span data-i18n="voice.panel.soundboard">Soundboard</span></div>
|
||
<div class="sb-sidebar-body">
|
||
<input type="text" id="sb-sidebar-search" data-i18n-placeholder="modals.sound_manager.search_placeholder" placeholder="Search sounds..." class="settings-text-input" style="width:100%;box-sizing:border-box;font-size:0.75rem">
|
||
<div id="sb-sidebar-grid" class="soundboard-grid sidebar-mode"></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Voice/users sidebar collapse arrow — fixed, sits at left edge of voice/users panel -->
|
||
<button id="sidebar-toggle-btn" class="sidebar-collapse-btn" data-i18n-title="app.actions.toggle_members_panel" data-i18n-aria-label="app.actions.toggle_members_panel" title="Toggle members panel" aria-label="Toggle members panel">❯</button>
|
||
|
||
<!-- ─── Right Sidebar ───────────────────────────── -->
|
||
<aside class="right-sidebar" id="right-sidebar" data-haven-region="context-sidebar">
|
||
<div class="right-sidebar-resize-handle" id="right-sidebar-resize-handle"></div>
|
||
<!-- Search results overlay — sits over voice + member list, persists across
|
||
channel switches, only closes on the X. (search-overhaul phase 1) -->
|
||
<div id="search-panel" class="search-panel" data-haven-region="search-results" style="display:none">
|
||
<div class="search-panel-header">
|
||
<span id="search-panel-count" data-i18n="header.results">Results</span>
|
||
<button id="search-panel-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
<div id="search-panel-banner" class="search-panel-banner" style="display:none">
|
||
<span data-i18n="header.search_stale">A channel or role you have has changed, please refresh the search to continue.</span>
|
||
<button id="search-rerun-btn" class="btn-secondary small" data-i18n="header.search_refresh">Refresh search</button>
|
||
</div>
|
||
<div id="search-panel-loading" class="search-panel-loading" style="display:none">
|
||
<span class="search-spinner"></span><span data-i18n="header.searching">Searching...</span>
|
||
</div>
|
||
<div id="search-panel-list" class="search-panel-list"></div>
|
||
<div id="search-panel-pager" class="search-panel-pager" style="display:none">
|
||
<button id="search-page-prev" class="icon-btn small" data-i18n-title="header.search_previous" title="Previous page">‹</button>
|
||
<span class="search-page-label">
|
||
<input id="search-page-input" class="search-page-input" type="text" inputmode="numeric" value="1" data-i18n-aria-label="header.search_page_number" aria-label="Page number">
|
||
<span id="search-page-total" class="search-page-total"></span>
|
||
</span>
|
||
<button id="search-page-next" class="icon-btn small" data-i18n-title="header.search_next" title="Next page">›</button>
|
||
</div>
|
||
</div>
|
||
<!-- Voice users section -->
|
||
<div class="right-sidebar-voice" id="right-sidebar-voice" data-haven-region="voice-roster">
|
||
<div class="panel">
|
||
<div class="voice-panel-header">
|
||
<h5 class="panel-title" style="margin-bottom:0"><span class="title-emoji">🔊 </span><span data-i18n="right_sidebar.voice_title">Voice</span></h5>
|
||
<button id="mobile-right-close" class="mobile-panel-close mobile-panel-close-right" data-i18n-title="modals.common.close" data-i18n-aria-label="right_sidebar.close_panel" title="Close" aria-label="Close panel">×</button>
|
||
<div class="voice-header-actions">
|
||
<button id="voice-mute-btn-header" class="voice-header-btn" data-i18n-title="voice.mute" title="Mute" style="display:none">🎙️</button>
|
||
<button id="voice-deafen-btn-header" class="voice-header-btn" data-i18n-title="voice.deafen" title="Deafen" style="display:none">🔊</button>
|
||
</div>
|
||
</div>
|
||
<button id="voice-join-mobile" class="btn-voice mobile-voice-join" style="display:none">🎤 <span data-i18n="voice.join">Join Voice</span></button>
|
||
<div id="voice-users" class="user-list">
|
||
<p class="muted-text" data-i18n="right_sidebar.no_one_in_voice">No one in voice</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Scrollable users section -->
|
||
<div class="right-sidebar-users" id="right-sidebar-users" data-haven-region="member-list">
|
||
<div class="panel">
|
||
<h5 class="panel-title"><span class="title-emoji">👥 </span><span data-i18n="right_sidebar.online_title">Online</span></h5>
|
||
<div class="user-search-wrap" id="user-search-wrap" style="display:none">
|
||
<input type="search" id="user-search" class="user-search-input" autocomplete="off" spellcheck="false"
|
||
data-i18n-placeholder="right_sidebar.search_members" placeholder="Search members...">
|
||
<button type="button" class="user-search-clear" id="user-search-clear" style="display:none"
|
||
data-i18n-title="right_sidebar.search_clear" title="Clear">×</button>
|
||
</div>
|
||
<div id="online-users" class="user-list">
|
||
<p class="muted-text" data-i18n="right_sidebar.select_channel">Select a channel</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Invite users button pinned under user list -->
|
||
<div class="panel sidebar-invite-panel" id="sidebar-invite-panel" style="display:none">
|
||
<button class="panel btn-invite-popout" id="right-btn-invite-popout" data-i18n-title="app.actions.invite" title="Invite Users">
|
||
<svg width="16" height="16" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<circle cx="14" cy="11" r="6"/>
|
||
<path d="M5,26c0-4.971,4.029-9,9-9 c1.864,0,3.596,0.567,5.032,1.537"/>
|
||
<circle cx="24" cy="24" r="7"/>
|
||
<line x1="24" y1="28" x2="24" y2="20"/>
|
||
<line x1="20" y1="24" x2="28" y2="24"/>
|
||
</svg>
|
||
<span data-i18n="app.actions.invite">Invite Users</span>
|
||
</button>
|
||
</div>
|
||
<!-- Voice settings sub-panel (slides up above controls) -->
|
||
<div id="voice-settings-panel" class="voice-settings-panel" data-haven-region="voice-settings" style="display:none">
|
||
<div class="voice-settings-section-label">🎧 <span data-i18n="voice_settings.audio_devices">Audio Devices</span></div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.input">Input</label>
|
||
<select id="voice-input-device" class="voice-settings-select">
|
||
<option value="" data-i18n="voice_settings.default_mic">Default Microphone</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.output">Output</label>
|
||
<select id="voice-output-device" class="voice-settings-select">
|
||
<option value="" data-i18n="voice_settings.default_speaker">Default Speaker</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.camera">Camera</label>
|
||
<select id="voice-cam-device" class="voice-settings-select">
|
||
<option value="" data-i18n="voice_settings.default_camera">Default Camera</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-divider"></div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label">🤫 <span data-i18n="voice_settings.noise_reduction">Noise Reduction</span></label>
|
||
<select id="voice-noise-mode" class="voice-settings-select" data-i18n-title="voice_settings.noise_mode_hint" title="Off = no processing, Gate = silence below threshold, Suppress = AI noise removal (RNNoise)">
|
||
<option value="off" data-i18n="voice_settings.off">Off</option>
|
||
<option value="gate" data-i18n="voice_settings.noise_gate">Noise Gate</option>
|
||
<option value="suppress" data-i18n="voice_settings.suppression_ai">Suppression (AI)</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row" id="noise-gate-row">
|
||
<label class="voice-settings-label" style="font-size:0.72rem;opacity:0.8" data-i18n="voice_settings.gate_sensitivity">Gate Sensitivity</label>
|
||
<input type="range" id="voice-ns-slider" class="ns-slider" min="0" max="100" value="10" data-i18n-title="voice_settings.gate_slider_hint" title="Noise gate: 0 = off, 100 = aggressive">
|
||
</div>
|
||
<div class="voice-settings-row mic-meter-row">
|
||
<label class="voice-settings-label">🎙️ <span data-i18n="voice_settings.mic_level">Mic Level</span></label>
|
||
<div class="mic-meter" id="mic-level-meter">
|
||
<div class="mic-meter-fill" id="mic-meter-fill"></div>
|
||
<div class="mic-meter-threshold" id="mic-meter-threshold" data-i18n-title="voice_settings.gate_threshold" title="Gate threshold"></div>
|
||
</div>
|
||
</div>
|
||
<div class="voice-settings-divider"></div>
|
||
<div class="voice-settings-section-label">🖥️ <span data-i18n="voice_settings.screen_share_quality">Screen Share Quality</span></div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.resolution">Resolution</label>
|
||
<select id="screen-res-select" class="voice-settings-select">
|
||
<option value="source" data-i18n="voice_settings.resolution_source">Source</option>
|
||
<option value="720">720p</option>
|
||
<option value="1080" selected>1080p</option>
|
||
<option value="1440">1440p</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.frame_rate">Frame Rate</label>
|
||
<select id="screen-fps-select" class="voice-settings-select">
|
||
<option value="15">15 fps</option>
|
||
<option value="30" selected>30 fps</option>
|
||
<option value="60">60 fps</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row" id="native-screen-share-row" hidden>
|
||
<label class="voice-settings-label" for="native-screen-share-enabled" data-i18n="voice_settings.native_screen_share">Native GPU sharing</label>
|
||
<input type="checkbox" id="native-screen-share-enabled">
|
||
</div>
|
||
<small class="settings-hint" id="native-screen-share-hint" data-i18n="voice_settings.native_screen_share_hint" hidden>Use the experimental Desktop native media pipeline. Chromium remains the default and fallback.</small>
|
||
</div>
|
||
<!-- Voice controls bar (pinned above settings) -->
|
||
<div id="voice-panel" class="voice-panel" data-haven-region="voice-controls" style="display:none">
|
||
<button id="voice-cam-btn" class="voice-panel-btn" data-i18n-title="voice.panel.camera" title="Camera">📷</button>
|
||
<button id="screen-share-btn" class="voice-panel-btn" data-i18n-title="voice.screen_share" title="Share Screen">🖥️</button>
|
||
<button id="voice-soundboard-btn" class="voice-panel-btn" data-i18n-title="voice.panel.soundboard" title="Soundboard">🎵</button>
|
||
<button id="voice-listen-together-btn" class="voice-panel-btn" data-i18n-title="voice.panel.listen_together" title="Listen Together">🎶</button>
|
||
<span class="voice-panel-divider"></span>
|
||
<button id="voice-settings-toggle" class="voice-panel-btn" data-i18n-title="voice.panel.settings" title="Voice Settings">⚙️</button>
|
||
<button id="voice-leave-sidebar-btn" class="voice-panel-btn voice-panel-leave" data-i18n-title="voice.leave" title="Leave Voice">✕</button>
|
||
</div>
|
||
<!-- Settings button pinned at bottom -->
|
||
<div class="panel sidebar-settings-panel">
|
||
<button class="btn-settings-popout" id="open-settings-btn" data-i18n-title="app.actions.settings" title="Settings">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||
</svg>
|
||
<span data-i18n="app.actions.settings">Settings</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Mobile overlay (click to close sidebars) -->
|
||
<div id="mobile-overlay" class="mobile-overlay"></div>
|
||
</div>
|
||
|
||
<!-- ─── Status Bar (bottom) ───────────────────────── -->
|
||
<div class="status-bar" id="status-bar" data-haven-region="status-bar" style="display:none">
|
||
<div class="status-item">
|
||
<span class="led on" id="status-server-led"></span>
|
||
<span class="label" data-i18n="status_bar.server">Server</span>
|
||
<span class="value" id="status-server-text" data-i18n="app.status.connected">Connected</span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="label" data-i18n="status_bar.ping">Ping</span>
|
||
<span class="value" id="status-ping">--</span>
|
||
<span class="label" data-i18n="status_bar.ping_unit">ms</span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="label" data-i18n="status_bar.channel">Channel</span>
|
||
<span class="value" id="status-channel" data-i18n="status_bar.channel_none">None</span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item status-online-trigger" id="status-online-trigger" data-i18n-title="status_bar.online_title" title="Click to see who's online">
|
||
<span class="label" data-i18n="status_bar.online">Online</span>
|
||
<span class="value" id="status-online-count">0</span>
|
||
</div>
|
||
<span class="spacer"></span>
|
||
<div class="status-item status-url-item" id="status-url-item">
|
||
<span class="value status-url-text" id="status-url-text" data-i18n-title="status_bar.copy_server_address" title="Click to copy server address"></span>
|
||
<button class="status-url-toggle" id="status-url-toggle" data-i18n-title="status_bar.toggle_server_address" title="Show/hide server address">👁</button>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="value" id="status-clock"></span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="value" id="status-version"></span>
|
||
</div>
|
||
</div>
|
||
<!-- Status bar toggle tab (visible when bar is hidden) -->
|
||
<button class="status-bar-toggle-tab" id="status-bar-toggle" data-i18n-title="status_bar.toggle_bar" title="Toggle status bar (debug footer)">📊</button>
|
||
|
||
<!-- Online users overlay (floats above status bar) -->
|
||
<div id="online-overlay" class="online-overlay" style="display:none">
|
||
<div class="online-overlay-header">
|
||
<span class="online-overlay-title" data-i18n="status_bar.users_overlay_title">Users</span>
|
||
<button id="online-overlay-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
<div id="online-overlay-list" class="online-overlay-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hidden audio container for voice streams -->
|
||
<div id="audio-container" style="display:none"></div>
|
||
|
||
<!-- Toast notifications -->
|
||
<div id="toast-container"></div>
|
||
|
||
<!-- Move Messages Toolbar (floating above messages) -->
|
||
<div id="move-msg-toolbar" class="move-msg-toolbar" style="display:none">
|
||
<span id="move-msg-count">0 selected</span>
|
||
<button class="btn-sm btn-accent" id="move-msg-move-btn" data-i18n="modals.move_messages.move_to">Move to...</button>
|
||
<button class="btn-sm btn-danger-fill" id="move-msg-delete-btn" data-i18n="modals.move_messages.delete_btn">Delete</button>
|
||
<button class="btn-sm" id="move-msg-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
|
||
<!-- Move Messages Channel Picker Modal -->
|
||
<div class="modal-overlay" id="move-msg-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.move_messages.title">Move Messages</h3>
|
||
<p class="modal-desc" id="move-msg-desc" data-i18n="modals.move_messages.desc">Select a destination channel</p>
|
||
<div class="move-msg-channel-list" id="move-msg-channel-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="move-msg-modal-cancel" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Thread Panel (slides in from right) -->
|
||
<!-- Pinned Messages PiP floating panel (draggable overlay) -->
|
||
<div id="pins-pip-panel" class="pins-pip-panel" style="display:none">
|
||
<div class="pins-pip-header">
|
||
<span class="pins-pip-icon" aria-hidden="true">📌</span>
|
||
<span id="pins-pip-title" class="pins-pip-title" data-i18n="header.pinned_count">Pinned Messages</span>
|
||
<div class="pins-pip-spacer"></div>
|
||
<button id="pins-pip-fullscreen" class="icon-btn small" data-i18n-title="media.toggle_fullscreen" title="Toggle fullscreen">⤢</button>
|
||
<button id="pins-pip-popin" class="icon-btn small" data-i18n-title="media.pop_back_sidebar" title="Pop back in to sidebar">⧈</button>
|
||
<button id="pins-pip-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
<div id="pins-pip-list" class="pins-pip-list pinned-list"></div>
|
||
</div>
|
||
<!-- DM PiP floating panel (overlay; doesn't switch the active channel) -->
|
||
<div id="dm-pip-panel" class="dm-pip-panel" style="display:none">
|
||
<div id="dm-pip-banner" class="dm-pip-banner" aria-hidden="true"></div>
|
||
<div class="dm-pip-header">
|
||
<div id="dm-pip-avatar-wrap" class="dm-pip-avatar-wrap" aria-hidden="true"></div>
|
||
<span id="dm-pip-title" class="dm-pip-title" data-i18n="dm_runtime.title">DM</span>
|
||
<div class="dm-pip-spacer"></div>
|
||
<button id="dm-pip-fullscreen" class="icon-btn small" data-i18n-title="media.open_full_pane" title="Open in full pane">▣</button>
|
||
<button id="dm-pip-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
<div id="dm-pip-messages" class="dm-pip-messages"></div>
|
||
<div id="dm-pip-reply-bar" class="dm-pip-reply-bar" style="display:none">
|
||
<span id="dm-pip-reply-preview-text"></span>
|
||
<button id="dm-pip-reply-close-btn" class="icon-btn small" data-i18n-aria-label="header.cancel_reply" aria-label="Cancel reply">×</button>
|
||
</div>
|
||
<div id="dm-pip-image-queue-bar" class="image-queue-bar" style="display:none"></div>
|
||
<div class="dm-pip-input-area">
|
||
<div class="pip-input-resizer" aria-hidden="true"></div>
|
||
<button id="dm-pip-emoji-btn" class="pip-input-emoji-btn" data-i18n-title="dm_runtime.emoji" title="Emoji"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"/><circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"/></svg></span></button>
|
||
<textarea id="dm-pip-input" class="dm-pip-input" data-i18n-placeholder="dm_runtime.reply_placeholder" placeholder="Reply…" rows="1"></textarea>
|
||
<button id="dm-pip-send" class="dm-pip-send" data-i18n-title="dm_runtime.send" title="Send">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="thread-panel" class="thread-panel" data-haven-region="thread-panel" style="display:none">
|
||
<div class="thread-panel-resizer" id="thread-panel-resizer" aria-hidden="true"></div>
|
||
<div class="thread-panel-header">
|
||
<div class="thread-panel-header-top">
|
||
<span class="thread-panel-icon">🧵</span>
|
||
<span id="thread-panel-title" class="thread-panel-title" data-i18n="thread_runtime.title">Thread</span>
|
||
<button id="thread-panel-pip" class="icon-btn small" data-i18n-title="thread_runtime.pop_out" title="Pop out thread (PiP)" aria-pressed="false">⧉</button>
|
||
<button id="thread-panel-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
<div class="thread-parent-meta" id="thread-parent-meta">
|
||
<div class="thread-parent-avatar-wrap" id="thread-parent-avatar-wrap"></div>
|
||
<span class="thread-parent-name" id="thread-parent-name" data-i18n="thread_runtime.starter">Thread starter</span>
|
||
</div>
|
||
<div class="thread-parent-preview" id="thread-parent-preview"></div>
|
||
</div>
|
||
<div class="thread-messages" id="thread-messages"></div>
|
||
<div class="thread-reply-bar" id="thread-reply-bar" style="display:none">
|
||
<span id="thread-reply-preview-text"></span>
|
||
<button id="thread-reply-close-btn" class="icon-btn small" data-i18n-aria-label="thread_runtime.cancel_reply" aria-label="Cancel thread reply">×</button>
|
||
</div>
|
||
<div id="thread-image-queue-bar" class="image-queue-bar" style="display:none"></div>
|
||
<div class="thread-input-area">
|
||
<div class="pip-input-resizer" aria-hidden="true"></div>
|
||
<button id="thread-emoji-btn" class="pip-input-emoji-btn" data-i18n-title="dm_runtime.emoji" title="Emoji"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"/><circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"/></svg></span></button>
|
||
<textarea id="thread-input" class="thread-input" data-i18n-placeholder="thread_runtime.reply_placeholder" placeholder="Reply in thread..." rows="1"></textarea>
|
||
<button id="thread-send-btn" class="thread-send-btn" data-i18n-title="dm_runtime.send" title="Send">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- First-Time Setup Wizard (Admin only) -->
|
||
<div class="modal-overlay" id="setup-wizard-modal" style="display:none">
|
||
<div class="modal modal-wizard">
|
||
<div class="wizard-header">
|
||
<span class="wizard-hex">⬡</span>
|
||
<h2 data-i18n="modals.wizard.title">Welcome to Haven</h2>
|
||
<p class="wizard-subtitle" data-i18n="modals.wizard.subtitle">Let's get your server ready. This only takes a minute.</p>
|
||
</div>
|
||
|
||
<div class="wizard-steps">
|
||
<!-- Step indicators -->
|
||
<div class="wizard-step-indicators">
|
||
<div class="wizard-indicator active" data-step="1">1</div>
|
||
<div class="wizard-indicator-line"></div>
|
||
<div class="wizard-indicator" data-step="2">2</div>
|
||
<div class="wizard-indicator-line"></div>
|
||
<div class="wizard-indicator" data-step="3">3</div>
|
||
<div class="wizard-indicator-line"></div>
|
||
<div class="wizard-indicator" data-step="4">4</div>
|
||
</div>
|
||
|
||
<!-- Step 1: Server basics -->
|
||
<div class="wizard-step" id="wizard-step-1" style="display:block">
|
||
<h3>🏠 <span data-i18n="modals.wizard.step1_title">Your Server</span></h3>
|
||
<p data-i18n="modals.wizard.step1_desc">Everything is already running. Here's what was set up automatically:</p>
|
||
<div class="wizard-checklist">
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_ssl">SSL certificates generated (encrypted connections)</span></div>
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_db">Database created</span></div>
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_keys">Security keys generated</span></div>
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_admin">Admin account created (that's you!)</span></div>
|
||
</div>
|
||
<div class="wizard-info-box">
|
||
<strong data-i18n="modals.wizard.server_name_label">Your server name:</strong>
|
||
<input type="text" id="wizard-server-name" class="wizard-input" value="Haven" placeholder="e.g. My Server, The Hideout" data-i18n-placeholder="modals.wizard.server_name_placeholder">
|
||
<span class="wizard-hint" data-i18n="modals.wizard.server_name_hint">This is what your friends see in their server list.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Create first channel -->
|
||
<div class="wizard-step" id="wizard-step-2" style="display:none">
|
||
<h3>📢 <span data-i18n="modals.wizard.step2_title">Create Your First Channel</span></h3>
|
||
<p data-i18n="modals.wizard.step2_desc">Channels are where conversations happen. Create one to get started.</p>
|
||
<div class="wizard-info-box">
|
||
<strong data-i18n="modals.wizard.channel_name_label">Channel name:</strong>
|
||
<input type="text" id="wizard-channel-name" class="wizard-input" value="General" placeholder="e.g. General, Gaming, Hangout" data-i18n-placeholder="modals.wizard.channel_name_placeholder">
|
||
<span class="wizard-hint" data-i18n="modals.wizard.channel_name_hint">You can create more channels later from the sidebar.</span>
|
||
</div>
|
||
<div id="wizard-channel-result" style="display:none">
|
||
<div class="wizard-check" id="wizard-channel-created">✅ <span data-i18n="modals.wizard.channel_created">Channel created!</span></div>
|
||
<div class="wizard-code-display">
|
||
<span data-i18n="modals.wizard.invite_code">Invite code:</span>
|
||
<code id="wizard-channel-code"></code>
|
||
<button id="wizard-copy-code" class="wizard-btn-small" data-i18n="modals.wizard.copy_btn">Copy</button>
|
||
</div>
|
||
<span class="wizard-hint" data-i18n="modals.wizard.share_hint">Share this code with friends so they can join this channel.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Connectivity check -->
|
||
<div class="wizard-step" id="wizard-step-3" style="display:none">
|
||
<h3>🌐 <span data-i18n="modals.wizard.step3_title">Internet Access</span></h3>
|
||
<p data-i18n="modals.wizard.step3_desc">Want friends outside your WiFi to connect? Let's check if your server is reachable from the internet.</p>
|
||
<div id="wizard-port-checking" style="display:none">
|
||
<div class="wizard-spinner"></div>
|
||
<span data-i18n="modals.wizard.checking_port">Checking port reachability...</span>
|
||
</div>
|
||
<div id="wizard-port-result" style="display:none"></div>
|
||
<button id="wizard-check-port-btn" class="wizard-btn-secondary">🔍 <span data-i18n="modals.wizard.check_connection_btn">Check My Connection</span></button>
|
||
<div class="wizard-info-box" style="margin-top:16px">
|
||
<span class="wizard-hint" data-i18n-html="modals.wizard.lan_only_hint"><strong>LAN only?</strong> If your friends are on the same WiFi, you can skip this — it will work automatically.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 4: You're ready -->
|
||
<div class="wizard-step" id="wizard-step-4" style="display:none">
|
||
<h3>🎉 <span data-i18n="modals.wizard.step4_title">You're All Set!</span></h3>
|
||
<p data-i18n="modals.wizard.step4_desc">Haven is running and ready for your friends.</p>
|
||
<div class="wizard-checklist">
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_configured">Server configured</span></div>
|
||
<div class="wizard-check" id="wizard-summary-channel">✅ <span data-i18n="modals.wizard.check_channel_created">First channel created</span></div>
|
||
<div class="wizard-check" id="wizard-summary-port">⏭️ <span data-i18n="modals.wizard.check_port_skipped">Port check skipped</span></div>
|
||
</div>
|
||
<div class="wizard-info-box">
|
||
<strong data-i18n="modals.wizard.how_friends_join">How friends join:</strong>
|
||
<ol class="wizard-steps-list">
|
||
<li><span data-i18n="modals.wizard.step_browser_prefix">They open</span> <code id="wizard-final-url">https://YOUR_IP:3000</code> <span data-i18n="modals.wizard.step_browser_suffix">in their browser</span></li>
|
||
<li data-i18n="modals.wizard.step_cert">Click "Advanced" → "Proceed" on the certificate warning</li>
|
||
<li data-i18n="modals.wizard.step_register">Register a username and password</li>
|
||
<li data-i18n="modals.wizard.step_code">Enter the channel invite code you share with them</li>
|
||
</ol>
|
||
</div>
|
||
<div class="wizard-info-box">
|
||
<span class="wizard-hint">💡 <span data-i18n-html="modals.wizard.tip"><strong>Tip:</strong> You can always access settings with the ⚙️ gear icon in the sidebar. This wizard won't show again.</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wizard-footer">
|
||
<button id="wizard-back-btn" class="wizard-btn-secondary" style="display:none" data-i18n="modals.wizard.back_btn">← Back</button>
|
||
<div style="flex:1"></div>
|
||
<button id="wizard-skip-btn" class="wizard-btn-text" data-i18n="modals.wizard.skip_btn">Skip Setup</button>
|
||
<button id="wizard-next-btn" class="wizard-btn-primary" data-i18n="modals.wizard.next_btn">Next →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel Code Settings Modal (Admin) -->
|
||
<div class="modal-overlay" id="code-settings-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.code_settings.title">Channel Code Settings</h3>
|
||
<p class="modal-desc" id="code-settings-channel-name"></p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.code_settings.visibility_label">Code Visibility</label>
|
||
<select id="code-visibility-select" class="form-select">
|
||
<option value="public" data-i18n="modals.code_settings.visibility_public">Public — everyone can see the code</option>
|
||
<option value="private" data-i18n="modals.code_settings.visibility_private">Private — only admins see the code</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.code_settings.mode_label">Code Mode</label>
|
||
<select id="code-mode-select" class="form-select">
|
||
<option value="static" data-i18n="modals.code_settings.mode_static">Static — code never changes</option>
|
||
<option value="dynamic" data-i18n="modals.code_settings.mode_dynamic">Dynamic — code rotates automatically</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="rotation-type-group" style="display:none">
|
||
<label data-i18n="modals.code_settings.rotation_label">Rotation Trigger</label>
|
||
<select id="code-rotation-type-select" class="form-select">
|
||
<option value="time" data-i18n="modals.code_settings.rotation_time">Time-based — rotate every X minutes</option>
|
||
<option value="joins" data-i18n="modals.code_settings.rotation_joins">Join-based — rotate after X new joins</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="rotation-interval-group" style="display:none">
|
||
<label id="rotation-interval-label" data-i18n="modals.code_settings.interval_label">Rotation Interval (minutes)</label>
|
||
<input type="number" id="code-rotation-interval" min="1" max="10000" value="60">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="code-settings-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="code-rotate-now-btn">🔄 <span data-i18n="modals.code_settings.rotate_now_btn">Rotate Now</span></button>
|
||
<button class="btn-sm btn-accent" id="code-settings-save-btn" data-i18n="modals.common.save">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Server Modal -->
|
||
<div class="modal-overlay" id="add-server-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 id="add-server-modal-title" data-i18n="modals.add_server.title">Add a Server</h3>
|
||
<p class="modal-desc" data-i18n="modals.add_server.desc">Connect to a friend's Haven server</p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.add_server.name_label">Server Name</label>
|
||
<input type="text" id="add-server-name-input" placeholder='e.g. Jake's Haven' maxlength="30">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.add_server.address_label">Server Address</label>
|
||
<input type="text" id="server-url-input" data-i18n-placeholder="modals.add_server.address_placeholder" placeholder="e.g. https://192.168.1.5:3000" list="known-servers-datalist">
|
||
<datalist id="known-servers-datalist"></datalist>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.add_server.icon_label">Icon URL</label> <span class="muted-text" data-i18n="modals.add_server.icon_optional">(optional)</span>
|
||
<input type="text" id="add-server-icon-input" data-i18n-placeholder="modals.add_server.icon_placeholder" placeholder="https://... or leave blank">
|
||
</div>
|
||
<label class="eula-check-row" style="margin-bottom:12px;gap:8px">
|
||
<input type="checkbox" id="server-auto-icon" checked>
|
||
<span data-i18n="modals.add_server.auto_icon">Auto-pull icon from server</span>
|
||
</label>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-server-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="save-server-btn" data-i18n="modals.add_server.add_btn">Add Server</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rename / Profile Modal -->
|
||
<div class="modal-overlay" id="rename-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.edit_profile.title">Edit Profile</h3>
|
||
|
||
<!-- Avatar Customizer -->
|
||
<div class="avatar-settings" style="margin-bottom: 16px;">
|
||
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.avatar_title">Avatar</h5>
|
||
<div class="avatar-upload-row" style="margin-bottom: 10px;">
|
||
<div class="avatar-upload-preview" id="avatar-upload-preview"></div>
|
||
<button type="button" class="btn-sm" id="avatar-upload-btn" data-i18n="modals.edit_profile.upload_btn">Upload</button>
|
||
<button type="button" class="btn-sm" id="avatar-remove-btn" data-i18n="modals.edit_profile.clear_btn">Clear</button>
|
||
<input type="file" id="avatar-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
|
||
</div>
|
||
<small class="muted-text" style="display:block;margin-top:-4px;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.avatar_hint">JPG, PNG, GIF (animated!), WebP · Max 2 MB</small>
|
||
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.border_title">Border</h5>
|
||
<div class="avatar-upload-row" style="margin-bottom: 10px;">
|
||
<div class="avatar-upload-preview" id="border-upload-preview"></div>
|
||
<button type="button" class="btn-sm" id="border-upload-btn" data-i18n="modals.edit_profile.upload_btn">Upload</button>
|
||
<button type="button" class="btn-sm" id="border-crop-btn" data-i18n="modals.edit_profile.edit_btn">Edit</button>
|
||
<button type="button" class="btn-sm" id="border-remove-btn" data-i18n="modals.edit_profile.clear_btn">Clear</button>
|
||
<input type="file" id="border-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
|
||
</div>
|
||
<small class="muted-text" style="display:block;margin-top:-4px;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.border_hint">JPG, PNG, GIF (animated!), WebP · Max 2 MB</small>
|
||
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.animate_title">Animated profile</h5>
|
||
<select id="animate-profile-select" class="form-select" style="width:100%;margin-bottom:8px" data-i18n-title="modals.edit_profile.animate_title" title="Animated profile">
|
||
<option value="trigger" data-i18n="modals.edit_profile.animate_trigger">Trigger</option>
|
||
<option value="disabled" data-i18n="modals.edit_profile.animate_disabled">Disabled</option>
|
||
</select>
|
||
<small class="muted-text" style="display:block;margin-bottom:6px;opacity:0.6" data-i18n="modals.edit_profile.animate_note_intro">Controls how animations on your profile will appear for every user on Haven. This applies to both your avatar and border, assuming you upload an animated image such as a GIF or APNG.</small>
|
||
<small class="muted-text" style="display:block;margin-bottom:4px;opacity:0.6" data-i18n="modals.edit_profile.animate_note_trigger">Trigger — Animations on your profile will loop when a user puts their cursor over your message, or opens your profile card.</small>
|
||
<small class="muted-text" style="display:block;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.animate_note_disabled">Disabled — Animations will be completely disabled for your profile. Existing animated images uploaded to your profile will remain untouched, however, only the first frame of the image will be displayed on your profile.</small>
|
||
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.shape_title">Shape</h5>
|
||
<div class="avatar-shape-picker" id="avatar-shape-picker">
|
||
<button type="button" class="avatar-shape-btn active" data-shape="circle" data-i18n-title="modals.edit_profile.shape_circle" title="Circle"><div class="shape-preview shape-circle"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="rounded" data-i18n-title="modals.edit_profile.shape_rounded" title="Rounded"><div class="shape-preview shape-rounded"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="squircle" data-i18n-title="modals.edit_profile.shape_squircle" title="Squircle"><div class="shape-preview shape-squircle"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="hex" data-i18n-title="modals.edit_profile.shape_hexagon" title="Hexagon"><div class="shape-preview shape-hex"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="diamond" data-i18n-title="modals.edit_profile.shape_diamond" title="Diamond"><div class="shape-preview shape-diamond"></div></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Display Name -->
|
||
<p class="modal-desc" data-i18n="modals.edit_profile.display_name_desc">Letters, numbers, underscores, and spaces (2–20 chars)</p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.edit_profile.display_name_label">Display Name</label>
|
||
<input type="text" id="rename-input" data-i18n-placeholder="modals.edit_profile.display_name_placeholder" placeholder="New display name..." maxlength="20">
|
||
</div>
|
||
|
||
<!-- Bio -->
|
||
<div class="form-group" style="margin-top:8px">
|
||
<label data-i18n="modals.edit_profile.bio_label">Bio</label> <span class="muted-text" data-i18n="modals.edit_profile.bio_max">(max 190 chars)</span>
|
||
<textarea id="edit-profile-bio" class="edit-profile-textarea" maxlength="190" data-i18n-placeholder="modals.edit_profile.bio_placeholder" placeholder="Tell people about yourself…" rows="2"></textarea>
|
||
</div>
|
||
|
||
<!-- Groups -->
|
||
<div class="groups-section" id="rename-modal-groups-section" style="margin-top:14px;border-top:1px solid var(--border);padding-top:12px">
|
||
<div class="groups-section-header">
|
||
<h5 class="settings-section-subtitle" style="margin:0" data-i18n="modals.edit_profile.groups_title">Groups</h5>
|
||
<button type="button" class="btn-sm" id="manage-groups-btn" data-i18n="modals.edit_profile.manage_groups_btn">Manage Groups</button>
|
||
<button type="button" class="btn-sm btn-accent" id="save-groups-btn" data-i18n="modals.edit_profile.save_groups_btn" style="display:none">Manage Groups</button>
|
||
</div>
|
||
<p class="muted-text" style="margin:6px 0 8px;font-size:.78rem;line-height:1.35" data-i18n="modals.edit_profile.groups_hint">
|
||
Groups are displayed as roles on your profile card. They don't grant permissions, but they can grant channel access!
|
||
</p>
|
||
<div id="user-profile-groups-list" class="user-profile-groups-list"></div>
|
||
<div id="user-profile-groups-manager" class="user-profile-groups-manager" style="display:none"></div>
|
||
</div>
|
||
|
||
<!-- Personas (#86, #5349) -->
|
||
<div class="personas-section" style="margin-top:14px;border-top:1px solid var(--border);padding-top:12px">
|
||
<div class="personas-section-header">
|
||
<h5 class="settings-section-subtitle" style="margin:0" data-i18n="modals.edit_profile.personas_title">👥 Personas</h5>
|
||
<button type="button" class="btn-sm" id="add-persona-btn" data-i18n="modals.edit_profile.add_persona">+ Add Persona</button>
|
||
</div>
|
||
<p class="muted-text" style="margin:6px 0 8px;font-size:.78rem;line-height:1.35" data-i18n-html="modals.edit_profile.personas_hint">
|
||
Type <code>::Name your message</code> in chat to send as that persona (or pick from the autocomplete that appears when you start a message with <code>::</code>). Your real account stays linked for moderation.
|
||
</p>
|
||
<div id="personas-list" class="personas-list"></div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<span id="avatar-save-status" class="settings-hint" style="flex:1"></span>
|
||
<button class="btn-sm" id="cancel-rename-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="save-rename-btn" data-i18n="modals.common.save">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Border Edit Modal. An append-only op log is the source of truth: each tool
|
||
appends one fraction-based op (crop / move / resize / rotate / distort),
|
||
folded in order as nested CSS wrappers. The right-side list shows the log
|
||
with per-entry undo. Nothing is baked; the same fold drives real rendering. -->
|
||
<div class="modal-overlay" id="border-crop-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.border_crop.title">Edit</h3>
|
||
<div class="border-crop-body">
|
||
<div class="border-crop-main">
|
||
<div class="border-crop-modes" id="border-crop-modes">
|
||
<button type="button" class="border-crop-mode-btn active" data-mode="crop" data-i18n="modals.border_crop.mode_crop">Crop</button>
|
||
<button type="button" class="border-crop-mode-btn" data-mode="move" data-i18n="modals.border_crop.mode_move">Move</button>
|
||
<button type="button" class="border-crop-mode-btn" data-mode="resize" data-i18n="modals.border_crop.mode_resize">Resize</button>
|
||
<button type="button" class="border-crop-mode-btn" data-mode="rotate" data-i18n="modals.border_crop.mode_rotate">Rotate</button>
|
||
<button type="button" class="border-crop-mode-btn" data-mode="distort" data-i18n="modals.border_crop.mode_distort">Distort</button>
|
||
<button type="button" class="border-crop-mode-btn" data-mode="opacity" data-i18n="modals.border_crop.mode_opacity">Opacity</button>
|
||
</div>
|
||
<button type="button" class="border-crop-autocrop-btn" id="border-crop-autocrop-btn" data-i18n="modals.border_crop.autocrop">Crop out invisible pixels</button>
|
||
<div class="border-crop-rotate-actions" id="border-crop-rotate-actions">
|
||
<button type="button" class="btn-sm" id="border-rotate-ccw">-90°</button>
|
||
<button type="button" class="btn-sm" id="border-rotate-cw">+90°</button>
|
||
</div>
|
||
<div class="border-crop-resize-actions" id="border-crop-resize-actions">
|
||
<label class="border-resize-anchor-label" for="border-resize-anchor" data-i18n="modals.border_crop.anchor_label">Resize anchor</label>
|
||
<select id="border-resize-anchor" class="form-select">
|
||
<option value="center" data-i18n="modals.border_crop.anchor_center">Center</option>
|
||
<option value="corner" data-i18n="modals.border_crop.anchor_corner">Corner</option>
|
||
</select>
|
||
</div>
|
||
<div class="border-crop-opacity-actions" id="border-crop-opacity-actions">
|
||
<input type="range" id="border-opacity-slider" min="1" max="100" value="100">
|
||
<span class="border-opacity-value" id="border-opacity-value">100%</span>
|
||
</div>
|
||
<div class="border-crop-stage">
|
||
<div class="border-crop-preview mode-crop" id="border-crop-preview">
|
||
<div class="border-crop-layers" id="border-crop-layers"></div>
|
||
<div class="border-crop-box" id="border-crop-box">
|
||
<div class="border-crop-handle crop-edge crop-top" data-edge="top" data-i18n-title="modals.border_crop.crop_top" title="Crop top"></div>
|
||
<div class="border-crop-handle crop-edge crop-right" data-edge="right" data-i18n-title="modals.border_crop.crop_right" title="Crop right"></div>
|
||
<div class="border-crop-handle crop-edge crop-bottom" data-edge="bottom" data-i18n-title="modals.border_crop.crop_bottom" title="Crop bottom"></div>
|
||
<div class="border-crop-handle crop-edge crop-left" data-edge="left" data-i18n-title="modals.border_crop.crop_left" title="Crop left"></div>
|
||
<div class="border-crop-handle corner-handle corner-tl" data-corner="tl" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
|
||
<div class="border-crop-handle corner-handle corner-tr" data-corner="tr" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
|
||
<div class="border-crop-handle corner-handle corner-bl" data-corner="bl" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
|
||
<div class="border-crop-handle corner-handle corner-br" data-corner="br" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
|
||
<div class="border-crop-handle scale-handle" data-i18n-title="modals.border_crop.mode_resize" title="Resize">⤢</div>
|
||
<div class="border-crop-handle rotate-handle" data-i18n-title="modals.border_crop.mode_rotate" title="Rotate">⟳</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="border-crop-side">
|
||
<h5 class="border-crop-side-title" data-i18n="modals.border_crop.history_title">Edits</h5>
|
||
<div class="border-crop-history" id="border-crop-history"></div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="border-crop-reset-btn" data-i18n="modals.border_crop.discard">Discard</button>
|
||
<button class="btn-sm btn-accent" id="border-crop-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Action Modal (Kick/Ban/Mute/Delete) -->
|
||
<div class="modal-overlay" id="admin-action-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 id="admin-action-title" data-i18n="modals.admin_action.title">Moderate User</h3>
|
||
<p class="modal-desc" id="admin-action-desc"></p>
|
||
<div class="form-group" id="admin-reason-group">
|
||
<label data-i18n="modals.admin_action.reason_label">Reason (optional)</label>
|
||
<input type="text" id="admin-action-reason" data-i18n-placeholder="modals.admin_action.reason_placeholder" placeholder="Reason..." maxlength="200">
|
||
</div>
|
||
<div class="form-group" id="admin-duration-group" style="display:none">
|
||
<label data-i18n="modals.admin_action.duration_label">Duration (minutes)</label>
|
||
<input type="number" id="admin-action-duration" value="10" min="1" max="43200" step="1">
|
||
</div>
|
||
<div class="form-group" id="admin-scrub-group" style="display:none">
|
||
<label class="toggle-row">
|
||
<span data-i18n="modals.admin_action.scrub_label">Scrub messages</span>
|
||
<input type="checkbox" id="admin-scrub-checkbox">
|
||
</label>
|
||
<div id="admin-scrub-scope-row" style="display:none;margin-top:6px;">
|
||
<label data-i18n="modals.admin_action.scope_label">Scope</label>
|
||
<select id="admin-scrub-scope" class="settings-select" style="width:100%;margin-top:4px;">
|
||
<option value="channel" data-i18n="modals.admin_action.scope_channel">This channel only</option>
|
||
<option value="server" data-i18n="modals.admin_action.scope_server">Server-wide</option>
|
||
</select>
|
||
</div>
|
||
<small class="settings-hint" data-i18n="modals.admin_action.scrub_hint">Removes the user's messages (archived/protected messages are preserved)</small>
|
||
</div>
|
||
<div class="form-group" id="admin-purge-group" style="display:none">
|
||
<label class="toggle-row">
|
||
<span data-i18n="modals.admin_action.purge_label">Replace messages with placeholder</span>
|
||
<input type="checkbox" id="admin-purge-checkbox">
|
||
</label>
|
||
<div id="admin-purge-message-row" style="display:none;margin-top:6px;">
|
||
<label data-i18n="modals.admin_action.purge_message_label">Replacement text (optional)</label>
|
||
<input type="text" id="admin-purge-message" maxlength="200" data-i18n-placeholder="modals.admin_action.purge_message_placeholder" placeholder="User banned.">
|
||
</div>
|
||
<small class="settings-hint" data-i18n="modals.admin_action.purge_hint">Replaces the user's messages with placeholder text instead of deleting them. Default: "User banned."</small>
|
||
</div>
|
||
<div class="form-group" id="admin-ban-ip-group" style="display:none">
|
||
<label class="toggle-row">
|
||
<span data-i18n="modals.admin_action.ban_ip_label">Also ban recent IP address(es)</span>
|
||
<input type="checkbox" id="admin-ban-ip-checkbox">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="modals.admin_action.ban_ip_hint">Bans up to the 5 most recently seen IPs for this user. Warning: an IP ban may affect other users on the same network (CGNAT, VPN, school/work). Requires the "ban_ip" permission.</small>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-admin-action-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent btn-danger-fill" id="confirm-admin-action-btn" data-i18n="modals.common.confirm">Confirm</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Popout Modal -->
|
||
<div class="modal-overlay" id="settings-modal" style="display:none">
|
||
<div class="modal modal-settings" data-haven-region="settings">
|
||
<div class="settings-header">
|
||
<h3>⚙️ <span data-i18n="settings.title">Settings</span></h3>
|
||
<div class="settings-tab-bar" id="settings-tab-bar">
|
||
<button class="settings-tab active" data-tab="user">👤 <span data-i18n="settings.tab.user">User</span></button>
|
||
<button class="settings-tab settings-tab-admin" data-tab="admin" style="display:none">🛡️ <span data-i18n="settings.tab.admin">Admin</span></button>
|
||
</div>
|
||
<button class="settings-close-btn" id="close-settings-btn" data-i18n-title="modals.common.close" data-i18n-aria-label="modals.common.close" title="Close" aria-label="Close">×</button>
|
||
</div>
|
||
|
||
<div class="settings-layout">
|
||
<nav class="settings-nav" id="settings-nav">
|
||
<!-- Grouped nav. The settings body is one long scrolling column and
|
||
these are scroll-to anchors, so every section that exists needs
|
||
an entry here — anything missing is findable only by scrolling
|
||
past it, which is how Activity ended up effectively hidden. -->
|
||
<div class="settings-nav-user">
|
||
<div class="settings-nav-group-label" data-i18n="settings.nav.group_appearance">Appearance</div>
|
||
<div class="settings-nav-item active" data-target="section-language">🗣️ <span data-i18n="settings.nav.language">Language</span></div>
|
||
<div class="settings-nav-item" data-target="section-layout">📐 <span data-i18n="settings.nav.layout">Layout & Density</span></div>
|
||
<div class="settings-nav-item" data-target="section-font-size">🔤 <span data-i18n="settings.nav.text_size">Text Size</span></div>
|
||
<div class="settings-nav-item" data-target="section-emoji-size">😀 <span data-i18n="settings.nav.emoji_size">Emoji Size</span></div>
|
||
<div class="settings-nav-item" data-target="section-role-display">🎨 <span data-i18n="settings.nav.role_display">Role Display</span></div>
|
||
<div class="settings-nav-item" data-target="section-toolbar-icons">🧰 <span data-i18n="settings.nav.toolbar_icons">Toolbar Icons</span></div>
|
||
<div class="settings-nav-item" data-target="section-image-display">🖼️ <span data-i18n="settings.nav.image_display">Image Display</span></div>
|
||
<div class="settings-nav-item" data-target="section-embed-size">📺 <span data-i18n="settings.nav.embeds_media">Embeds & Media</span></div>
|
||
<div class="settings-nav-item" data-target="section-statusbar">📊 <span data-i18n="settings.nav.status_bar">Status Bar</span></div>
|
||
|
||
<div class="settings-nav-group-label" data-i18n="settings.nav.group_chat">Chat</div>
|
||
<div class="settings-nav-item" data-target="section-chat-behavior">💬 <span data-i18n="settings.nav.chat_behavior">Chat Behavior</span></div>
|
||
<div class="settings-nav-item" data-target="section-soundboard-mode">🎛️ <span data-i18n="settings.nav.soundboard">Soundboard</span></div>
|
||
<div class="settings-nav-item" data-target="section-sounds">🔔 <span data-i18n="settings.nav.sounds">Sounds</span></div>
|
||
<div class="settings-nav-item" data-target="section-push">📲 <span data-i18n="settings.nav.push">Push</span></div>
|
||
|
||
<div class="settings-nav-group-label" data-i18n="settings.nav.group_privacy_presence">Privacy & Presence</div>
|
||
<div class="settings-nav-item" data-target="section-activity">🎮 <span data-i18n="settings.nav.activity">Activity</span></div>
|
||
<div class="settings-nav-item" data-target="section-score-badges">🚢 <span data-i18n="settings.nav.score_badges">Score Badges</span></div>
|
||
|
||
<div class="settings-nav-group-label" data-i18n="settings.nav.group_account_security">Account & Security</div>
|
||
<div class="settings-nav-item" data-target="section-password">🔒 <span data-i18n="settings.nav.password">Password</span></div>
|
||
<div class="settings-nav-item" data-target="section-sessions">💻 <span data-i18n="settings.nav.sessions">Sessions</span></div>
|
||
<div class="settings-nav-item" data-target="section-2fa">🔐 <span data-i18n="settings.nav.two_factor">Two-Factor</span></div>
|
||
<div class="settings-nav-item" data-target="section-recovery">🔑 <span data-i18n="settings.nav.recovery">Recovery</span></div>
|
||
<div class="settings-nav-item" data-target="section-delete-account">⚠️ <span data-i18n="settings.nav.account">Account</span></div>
|
||
|
||
<div class="settings-nav-group-label" data-i18n="settings.nav.group_advanced">Advanced</div>
|
||
<div class="settings-nav-item" data-target="section-plugins">🧩 <span data-i18n="settings.nav.plugins">Plugins & Themes</span></div>
|
||
<div class="settings-nav-item" data-target="section-desktop-shortcuts" id="desktop-shortcuts-nav" style="display:none">⌨️ <span data-i18n="settings.nav.shortcuts">Shortcuts</span></div>
|
||
<div class="settings-nav-item" data-target="section-desktop-app" id="desktop-app-nav" style="display:none">🖥️ <span data-i18n="settings.nav.desktop_app">Desktop App</span></div>
|
||
<div class="settings-nav-item" data-target="section-debug">🐞 <span data-i18n="settings.nav.debug">Debug</span></div>
|
||
</div>
|
||
<div class="settings-nav-admin-group" style="display:none">
|
||
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_server">Server</div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-update" style="display:none">⬆️ <span data-i18n="settings.nav.server_updates">Server Updates</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-branding" style="display:none">🏠 <span data-i18n="settings.nav.branding">Branding</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-connectivity" style="display:none">📡 <span data-i18n="settings.nav.connectivity">Voice & Connectivity</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-tunnel" style="display:none">🧭 <span data-i18n="settings.nav.tunnel">Tunnel</span></div>
|
||
|
||
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_people">People & Access</div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-members" style="display:none">👥 <span data-i18n="settings.nav.members">Members</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-roles" style="display:none">👑 <span data-i18n="settings.nav.roles">Roles</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-moderation" style="display:none">🛡️ <span data-i18n="settings.nav.moderation">Moderation</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-security" style="display:none">🔒 <span data-i18n="settings.nav.security">Security</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-automod" style="display:none">🛡️ <span data-i18n="settings.nav.automod">Moderation</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-whitelist" style="display:none">🛡️ <span data-i18n="settings.nav.whitelist">Whitelist</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-invite" style="display:none">🌐 <span data-i18n="settings.nav.invite">Invite Code</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-guests" style="display:none">👤 <span data-i18n="settings.nav.guests">Guest Access</span></div>
|
||
|
||
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_content">Content</div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-emojis" style="display:none">😎 <span data-i18n="settings.nav.emojis">Emojis</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-stickers" style="display:none">🖼️ <span data-i18n="settings.nav.stickers">Stickers</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-sounds-admin" style="display:none">🔊 <span data-i18n="settings.nav.admin_sounds">Sounds</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-uploads" style="display:none">📁 <span data-i18n="settings.nav.limits">Limits</span></div>
|
||
|
||
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_integrations">Integrations</div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-bots" style="display:none">🤖 <span data-i18n="settings.nav.bots">Bots</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-ferry" style="display:none">🛶 <span data-i18n="settings.nav.ferry">Ferry</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-import" style="display:none">📦 <span data-i18n="settings.nav.import">Import</span></div>
|
||
|
||
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_maintenance">Maintenance</div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-backup" style="display:none">💾 <span data-i18n="settings.nav.backup">Backup</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-cleanup" style="display:none">🗑️ <span data-i18n="settings.nav.cleanup">Cleanup</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-modmode" style="display:none">🔧 <span data-i18n="settings.nav.mod_mode">Mod Mode</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-audit-log" style="display:none">📜 <span data-i18n="settings.nav.audit_log">Audit Log</span></div>
|
||
</div>
|
||
</nav>
|
||
<div class="settings-body" id="settings-body-user">
|
||
|
||
<!-- Language Section -->
|
||
<div class="settings-section" id="section-language">
|
||
<h5 class="settings-section-title">🗣️ <span data-i18n="settings.language_section.title">Language</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.language_section.label">Language</span>
|
||
<select id="language-select" class="settings-select">
|
||
<option value="auto" data-i18n="settings.language_section.auto">Automatic</option>
|
||
<option value="en">English</option>
|
||
<option value="fr">Français</option>
|
||
<option value="de">Deutsch</option>
|
||
<option value="es">Español</option>
|
||
<option value="pl">Polski</option>
|
||
<option value="ru">Русский</option>
|
||
<option value="zh">中文</option>
|
||
<option value="pt">Português (Brasil)</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.language_section.hint">Changes apply immediately</small>
|
||
</div>
|
||
|
||
<div class="settings-section" id="section-layout">
|
||
<h5 class="settings-section-title">📐 <span data-i18n="settings.layout.title">Layout</span></h5>
|
||
<div class="settings-subsection" id="layout-density-subsection">
|
||
<small class="settings-group-label">📏 <span data-i18n="settings.layout.density.title">Layout Density</span></small>
|
||
<div class="density-picker" id="density-picker">
|
||
<button type="button" class="density-btn" data-density="compact" data-i18n-title="settings.layout.density.compact_hint" title="Compact — tighter spacing, smaller avatars">
|
||
<span class="density-icon">▪️</span>
|
||
<span class="density-label" data-i18n="settings.layout.density.compact">Compact</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-density="cozy" data-i18n-title="settings.layout.density.cozy_hint" title="Cozy — default balanced layout">
|
||
<span class="density-icon">◾</span>
|
||
<span class="density-label" data-i18n="settings.layout.density.cozy">Cozy</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-density="spacious" data-i18n-title="settings.layout.density.spacious_hint" title="Spacious — more breathing room">
|
||
<span class="density-icon">⬛</span>
|
||
<span class="density-label" data-i18n="settings.layout.density.spacious">Spacious</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="settings-subsection" id="channel-scrolling-subsection">
|
||
<small class="settings-group-label">📜 <span data-i18n="settings.layout.channel_scrolling.title">Channel Scrolling</span></small>
|
||
<div class="channel-scroll-picker" id="channel-scroll-picker">
|
||
<button type="button" class="density-btn active" data-channel-scroll="separate" data-i18n-title="settings.layout.channel_scrolling.separate_hint" title="Separate — separate scrollbars for channels and DMs">
|
||
<span class="density-icon">📜📜</span>
|
||
<span class="density-label" data-i18n="settings.layout.channel_scrolling.separate">Separate</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-channel-scroll="combined" data-i18n-title="settings.layout.channel_scrolling.combined_hint" title="Combined — one scrollbar for both channels and DMs">
|
||
<span class="density-icon">📜</span>
|
||
<span class="density-label" data-i18n="settings.layout.channel_scrolling.combined">Combined</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toggle Style (sliders vs checkboxes) -->
|
||
<div class="settings-section" id="section-toggle-style" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🎚️ <span data-i18n="settings.toggle_style.title">Toggle Style</span></h5>
|
||
<div class="font-size-picker" id="toggle-style-picker">
|
||
<button type="button" class="density-btn active" data-togglestyle="switch" data-i18n-title="settings.toggle_style.switch_hint" title="Sliding switches (default)">
|
||
<span class="density-icon">🎚️</span>
|
||
<span class="density-label" data-i18n="settings.toggle_style.switch">Sliders</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-togglestyle="box" data-i18n-title="settings.toggle_style.box_hint" title="Classic square checkboxes">
|
||
<span class="density-icon">☑️</span>
|
||
<span class="density-label" data-i18n="settings.toggle_style.box">Boxes</span>
|
||
</button>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.toggle_style.hint">Applies to on/off settings. Lists you tick items in, like permissions and channel pickers, always stay as boxes.</small>
|
||
</div>
|
||
|
||
<!-- Soundboard Sidebar Setting -->
|
||
<div class="settings-section" id="section-soundboard-mode" style="border-top: 1px solid var(--border-light); padding-top: 14px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🎵 <span data-i18n="settings.soundboard_section.title">Soundboard</span></h5>
|
||
<label class="checkbox-row" style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:0.8125rem">
|
||
<input type="checkbox" id="soundboard-sidebar-mode-settings" style="cursor:pointer">
|
||
<span data-i18n="settings.soundboard_section.sidebar_mode">Sidebar mode</span>
|
||
</label>
|
||
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.soundboard_section.sidebar_mode_hint">Open the soundboard as a pinned sidebar panel instead of a popup.</small>
|
||
</div>
|
||
|
||
<!-- Font Size -->
|
||
<div class="settings-section" id="section-font-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🔍 <span data-i18n="settings.zoom.title">Zoom</span></h5>
|
||
<div class="zoom-control" id="zoom-control">
|
||
<button type="button" class="zoom-step" id="zoom-out-btn" title="Zoom out" aria-label="Zoom out" data-i18n-title="settings.zoom.out" data-i18n-aria-label="settings.zoom.out">−</button>
|
||
<input type="range" class="zoom-slider" id="ui-zoom-slider" min="70" max="150" step="5" value="100" aria-label="Interface zoom" data-i18n-aria-label="settings.zoom.slider_label">
|
||
<button type="button" class="zoom-step" id="zoom-in-btn" title="Zoom in" aria-label="Zoom in" data-i18n-title="settings.zoom.in" data-i18n-aria-label="settings.zoom.in">+</button>
|
||
<output class="zoom-value" id="ui-zoom-value" for="ui-zoom-slider">100%</output>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.zoom.hint">Scales the whole interface. Drag for the exact size you want.</small>
|
||
</div>
|
||
|
||
<!-- Emoji Reaction Size -->
|
||
<div class="settings-section" id="section-emoji-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">😀 <span data-i18n="settings.reaction_size.title">Reaction Size</span></h5>
|
||
<div class="font-size-picker" id="emoji-size-picker">
|
||
<button type="button" class="density-btn" data-emojisize="small" title="Small reactions" data-i18n-title="settings.reaction_size.small_hint">
|
||
<span class="density-icon" style="font-size:0.6875rem">😀</span>
|
||
<span class="density-label" data-i18n="settings.reaction_size.small">Small</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-emojisize="normal" title="Normal reactions (default)" data-i18n-title="settings.reaction_size.normal_hint">
|
||
<span class="density-icon" style="font-size:0.875rem">😀</span>
|
||
<span class="density-label" data-i18n="settings.reaction_size.normal">Normal</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-emojisize="large" title="Large reactions" data-i18n-title="settings.reaction_size.large_hint">
|
||
<span class="density-icon" style="font-size:1.125rem">😀</span>
|
||
<span class="density-label" data-i18n="settings.reaction_size.large">Large</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-emojisize="x-large" title="Extra large reactions" data-i18n-title="settings.reaction_size.xl_hint">
|
||
<span class="density-icon" style="font-size:1.375rem">😀</span>
|
||
<span class="density-label" data-i18n="settings.reaction_size.xl">XL</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Role Color Display -->
|
||
<div class="settings-section" id="section-role-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🎨 <span data-i18n="settings.role_display.title">Role Display</span></h5>
|
||
<div class="density-picker" id="role-display-picker">
|
||
<button type="button" class="density-btn active" data-roledisplay="colored-name" title="Color the username with the role color" data-i18n-title="settings.role_display.colored_name_hint">
|
||
<span class="density-icon" style="color:#5865f2;font-weight:bold">A</span>
|
||
<span class="density-label" data-i18n="settings.role_display.colored_name">Colored Name</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-roledisplay="dot" title="Show a colored dot next to the name (legacy)" data-i18n-title="settings.role_display.dot_hint">
|
||
<span class="density-icon">●</span>
|
||
<span class="density-label" data-i18n="settings.role_display.dot">Dot</span>
|
||
</button>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.role_display.hint">How role colors are shown next to usernames in chat and the member list.</small>
|
||
</div>
|
||
|
||
<!-- Toolbar Icon Style -->
|
||
<div class="settings-section" id="section-toolbar-icons" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🎛️ <span data-i18n="settings.toolbar_icons.title">Toolbar Icons</span></h5>
|
||
<div class="density-picker" id="toolbar-icon-picker">
|
||
<button type="button" class="density-btn active" data-toolbaricons="mono" title="Use sleek monochrome icons in message toolbars" data-i18n-title="settings.toolbar_icons.monochrome_hint">
|
||
<span class="density-icon" aria-hidden="true">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||
<circle cx="12" cy="12" r="9"></circle>
|
||
<path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-linecap="round"></path>
|
||
<circle cx="9.2" cy="10.2" r="1" fill="currentColor" stroke="none"></circle>
|
||
<circle cx="14.8" cy="10.2" r="1" fill="currentColor" stroke="none"></circle>
|
||
</svg>
|
||
</span>
|
||
<span class="density-label" data-i18n="settings.toolbar_icons.monochrome">Monochrome</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-toolbaricons="emoji" title="Use colorful emoji icons in message toolbars" data-i18n-title="settings.toolbar_icons.colorful_hint">
|
||
<span class="density-icon">😀</span>
|
||
<span class="density-label" data-i18n="settings.toolbar_icons.colorful">Colorful Emoji</span>
|
||
</button>
|
||
</div>
|
||
<label class="toolbar-slots-row" for="toolbar-visible-slots">
|
||
<span data-i18n="settings.toolbar_icons.visible_slots">Visible toolbar slots before overflow</span>
|
||
<input type="range" id="toolbar-visible-slots" min="1" max="7" step="1" value="3">
|
||
<span id="toolbar-visible-slots-value" class="toolbar-slots-value">3</span>
|
||
</label>
|
||
<div class="toolbar-order-wrap">
|
||
<div class="toolbar-order-head">
|
||
<span data-i18n="settings.toolbar_icons.order">Toolbar slot order</span>
|
||
<button type="button" class="btn-sm" id="toolbar-order-reset-btn" data-i18n="settings.toolbar_icons.reset">Reset</button>
|
||
</div>
|
||
<div id="toolbar-order-list" class="toolbar-order-list"></div>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.toolbar_icons.hint">Applies to message and thread hover action bars.</small>
|
||
</div>
|
||
|
||
<div class="settings-section" id="section-image-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🖼️ <span data-i18n="settings.image_display.title">Image Display</span></h5>
|
||
|
||
<!-- Image Display Mode -->
|
||
<!-- Renamed from the Layout Density section's id, which it duplicated.
|
||
getElementById always resolved to the first match, so this section
|
||
had no working nav anchor at all. -->
|
||
<div class="settings-subsection" id="image-mode-subsection">
|
||
<small class="settings-group-label">🖼️ <span data-i18n="settings.image_display.image_mode.title">Image Display Mode</span></small>
|
||
<div class="density-picker" id="image-mode-picker">
|
||
<button type="button" class="density-btn active" data-image-mode="thumbnail" data-i18n-title="settings.image_display.image_mode.thumbnail_hint" title="Thumbnail — small preview, click to enlarge">
|
||
<span class="density-icon">🔍</span>
|
||
<span class="density-label" data-i18n="settings.image_display.image_mode.thumbnail">Thumbnail</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-image-mode="full" data-i18n-title="settings.image_display.image_mode.full_hint" title="Full — large inline images like Discord">
|
||
<span class="density-icon">🖼️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.image_mode.full">Full</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Animated profile pictures (viewer side). The pfp owner can already turn
|
||
their own animation off for everyone; this is the other half, so a
|
||
viewer who doesn't want moving avatars can stop seeing them. -->
|
||
<div class="settings-subsection" id="animate-pfp-subsection">
|
||
<small class="settings-group-label">✨ <span data-i18n="settings.image_display.animate_pfp.title">Animated Profile Pictures</span></small>
|
||
<div class="density-picker" id="animate-pfp-picker">
|
||
<button type="button" class="density-btn" data-animpfp="always" data-i18n-title="settings.image_display.animate_pfp.always_hint" title="Always: animated avatars loop constantly">
|
||
<span class="density-icon">▶️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.animate_pfp.always">Always</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-animpfp="hover" data-i18n-title="settings.image_display.animate_pfp.hover_hint" title="On hover: play while you hover a message or open a profile">
|
||
<span class="density-icon">🖱️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.animate_pfp.hover">On hover</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-animpfp="never" data-i18n-title="settings.image_display.animate_pfp.never_hint" title="Never: show the first frame only">
|
||
<span class="density-icon">⏸️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.animate_pfp.never">Never</span>
|
||
</button>
|
||
</div>
|
||
<small class="settings-hint" data-i18n="settings.image_display.animate_pfp.hint">Applies to everyone's avatars and borders as you see them. Anyone who has turned animation off for their own profile stays still regardless.</small>
|
||
</div>
|
||
|
||
<!-- Animated images in chat (#5526). Separate from the avatar setting above:
|
||
a GIF someone posted is content, an avatar is ambient, so this one
|
||
stays at Always unless the viewer asks otherwise. -->
|
||
<div class="settings-subsection" id="animate-chat-subsection">
|
||
<small class="settings-group-label">🎞️ <span data-i18n="settings.image_display.animate_chat.title">Animated Images in Chat</span></small>
|
||
<div class="density-picker" id="animate-chat-picker">
|
||
<button type="button" class="density-btn active" data-animchat="always" data-i18n-title="settings.image_display.animate_chat.always_hint" title="Always: GIFs loop constantly">
|
||
<span class="density-icon">▶️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.animate_chat.always">Always</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-animchat="hover" data-i18n-title="settings.image_display.animate_chat.hover_hint" title="On hover: play only while you point at the image">
|
||
<span class="density-icon">🖱️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.animate_chat.hover">On hover</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-animchat="never" data-i18n-title="settings.image_display.animate_chat.never_hint" title="Never: show the first frame only">
|
||
<span class="density-icon">⏸️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.animate_chat.never">Never</span>
|
||
</button>
|
||
</div>
|
||
<small class="settings-hint" data-i18n="settings.image_display.animate_chat.hint">Applies to GIFs and other animated images posted in messages, for you only. Nobody else's view changes.</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Embed / Link Preview Size -->
|
||
<div class="settings-section" id="section-embed-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🔗 <span data-i18n="settings.embed_display.title">Link Previews</span></h5>
|
||
<div class="density-picker" id="embed-size-picker">
|
||
<button type="button" class="density-btn" data-embed-size="off" title="Off — hide all link previews" data-i18n-title="settings.embed_display.off_hint">
|
||
<span class="density-icon">🚫</span>
|
||
<span class="density-label" data-i18n="settings.embed_display.off">Off</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-embed-size="small" title="Small — compact card with small media" data-i18n-title="settings.embed_display.small_hint">
|
||
<span class="density-icon">🔗</span>
|
||
<span class="density-label" data-i18n="settings.embed_display.small">Small</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-embed-size="medium" title="Medium — balanced card with medium media" data-i18n-title="settings.embed_display.medium_hint">
|
||
<span class="density-icon">🖼️</span>
|
||
<span class="density-label" data-i18n="settings.embed_display.medium">Medium</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-embed-size="full" title="Full — large media, author, and post stats" data-i18n-title="settings.embed_display.full_hint">
|
||
<span class="density-icon">📰</span>
|
||
<span class="density-label" data-i18n="settings.embed_display.full">Full</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Banner Display (client-side) -->
|
||
<div class="settings-section" id="section-banner-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px; display:none;">
|
||
<h5 class="settings-section-title">🏞️ <span data-i18n="settings.banner_display.title">Banner Display</span></h5>
|
||
<label style="margin-top:4px;display:flex;align-items:center;gap:8px;font-size:0.75rem">
|
||
<span style="white-space:nowrap" data-i18n="settings.banner_display.height">Banner height</span>
|
||
<input type="range" id="banner-height-slider" min="80" max="400" step="10" value="180" style="flex:1">
|
||
<span id="banner-height-value" style="min-width:36px;text-align:right;font-variant-numeric:tabular-nums">180px</span>
|
||
</label>
|
||
<label style="margin-top:4px;display:flex;align-items:center;gap:8px;font-size:0.75rem">
|
||
<span style="white-space:nowrap" data-i18n="settings.banner_display.offset">Vertical offset</span>
|
||
<input type="range" id="banner-offset-slider" min="0" max="100" step="1" value="0" style="flex:1">
|
||
<span id="banner-offset-value" style="min-width:30px;text-align:right;font-variant-numeric:tabular-nums">0%</span>
|
||
</label>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.banner_display.header_style">Header style</span>
|
||
<select id="banner-header-mode" style="max-width:160px">
|
||
<option value="full" data-i18n="settings.banner_display.full">Full Header (opaque)</option>
|
||
<option value="shaded" data-i18n="settings.banner_display.shaded">Shaded Header</option>
|
||
<option value="minimal" data-i18n="settings.banner_display.minimal">Minimal Header</option>
|
||
<option value="transparent" data-i18n="settings.banner_display.transparent">Transparent Header</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.banner_display.hint">How the banner interacts with the channel header area. These settings are per-device.</small>
|
||
</div>
|
||
|
||
<!-- Chat Behavior -->
|
||
<div class="settings-section" id="section-chat-behavior" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">💬 <span data-i18n="settings.chat_behavior.title">Chat</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.chat_behavior.up_arrow_edit">Up Arrow Edits Last Message</span>
|
||
<input type="checkbox" id="up-arrow-edit" checked>
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.up_arrow_edit_hint">Press the Up arrow key on an empty input to quickly edit your last message.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.chat_behavior.dm_fullscreen">Open DMs in fullscreen on single click</span>
|
||
<input type="checkbox" id="dm-fullscreen-default">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.dm_fullscreen_hint">Single-click a DM to open it in the full pane instead of a floating PiP. Double-click opens the PiP.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.chat_behavior.hover_profile_card">Show profile card on hover</span>
|
||
<input type="checkbox" id="hover-profile-card" checked>
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.hover_profile_card_hint">Hovering over a name or avatar shows a quick profile preview. Clicking opens the full card either way.</small>
|
||
</div>
|
||
|
||
<!-- Status Bar -->
|
||
<div class="settings-section" id="section-statusbar" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">📊 <span data-i18n="settings.status_bar_section.title">Status Bar</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.status_bar_section.show">Show Status Bar</span>
|
||
<input type="checkbox" id="show-status-bar">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.status_bar_section.hint">Show the status bar at the bottom of the screen with server, ping, channel, and online info.</small>
|
||
</div>
|
||
|
||
<!-- Game Score Badges -->
|
||
<div class="settings-section" id="section-score-badges" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🚢 <span data-i18n="settings.score_badges.title">Score Badges</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.score_badges.hide_others">Hide other players' score badges</span>
|
||
<input type="checkbox" id="hide-other-scores">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.score_badges.hide_others_hint">Don't show 🚢 badges next to other users in the member list.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.score_badges.hide_own">Hide my score badge from this server</span>
|
||
<input type="checkbox" id="hide-own-score">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.score_badges.hide_own_hint">Other users (and you) won't see your 🚢 badge anywhere on this server, including the leaderboard.</small>
|
||
</div>
|
||
|
||
<!-- Activity / Rich Presence -->
|
||
<div class="settings-section" id="section-activity" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🎮 <span data-i18n="settings.activity_section.title">Activity</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.activity_section.share">Share my activity</span>
|
||
<input type="checkbox" id="share-activity">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.activity_section.share_hint">Others can see what you're playing or listening to. Games and music only appear once you link an account below. Nothing is ever shared while your status is Invisible.</small>
|
||
|
||
<div id="activity-suboptions" style="display:none;padding-left:10px;border-left:2px solid var(--border-light);margin-bottom:10px">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.activity_section.show_games">Show games</span>
|
||
<input type="checkbox" id="share-game-activity">
|
||
</label>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.activity_section.show_music">Show music</span>
|
||
<input type="checkbox" id="share-music-activity">
|
||
</label>
|
||
</div>
|
||
|
||
<h5 class="settings-section-title" style="margin-top:12px">🔗 <span data-i18n="settings.activity_section.connections">Connections</span></h5>
|
||
<div id="connections-list">
|
||
<small class="settings-hint" data-i18n="modals.common.loading">Loading…</small>
|
||
</div>
|
||
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.activity_section.connections_hint">Haven's own music player needs no connection — it shares automatically while you're listening in a voice channel.</small>
|
||
|
||
<!-- Listening presence (any music player) -->
|
||
<div id="listening-connection" style="margin-top:14px">
|
||
<h5 class="settings-section-title">🎶 <span data-i18n="settings.listening.title">Listening Webhook</span></h5>
|
||
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n-html="settings.listening.note">Please refer to the <a href="https://github.com/ancsemi/Haven/blob/main/docs/listening-api.md" target="_blank" rel="noopener noreferrer">Listening API</a>.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.listening.toggle">Show my music on Haven</span>
|
||
<input type="checkbox" id="listening-enabled">
|
||
</label>
|
||
<div id="listening-url-row" hidden style="margin-top:8px">
|
||
<label class="connection-field">
|
||
<span data-i18n="settings.listening.url_label">Your webhook URL</span>
|
||
<input type="text" id="listening-url" readonly autocomplete="off" spellcheck="false">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-top:4px" data-i18n="settings.listening.url_hint">Point your player's plugin or script at this URL. Keep it secret; anyone with it can post to your profile.</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sounds Section -->
|
||
<div class="settings-section" id="section-sounds">
|
||
<h5 class="settings-section-title">🔔 <span data-i18n="settings.sounds_section.title">Sounds</span></h5>
|
||
<div class="notif-settings">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.notifications">Notifications</span>
|
||
<input type="checkbox" id="notif-enabled">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.notifications_hint">General message sounds (off by default). @Mentions, replies, and DMs always notify.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.mute_server">Mute this server</span>
|
||
<input type="checkbox" id="notif-server-muted">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.mute_server_hint">Suppress all notifications, sounds, and taskbar badge from this server.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.mentions">@Mentions</span>
|
||
<input type="checkbox" id="notif-mentions-enabled" checked>
|
||
</label>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.replies">Replies</span>
|
||
<input type="checkbox" id="notif-replies-enabled" checked>
|
||
</label>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.direct_messages">Direct Messages</span>
|
||
<input type="checkbox" id="notif-dm-enabled" checked>
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.popup_limit">Limit pop-up notifications</span>
|
||
<select id="notif-popup-cooldown">
|
||
<option value="0" data-i18n="settings.sounds_section.popup_off">Off (show every one)</option>
|
||
<option value="30000" data-i18n="settings.sounds_section.popup_30s">At most 1 every 30 sec</option>
|
||
<option value="60000" data-i18n="settings.sounds_section.popup_1m">At most 1 per minute</option>
|
||
<option value="300000" data-i18n="settings.sounds_section.popup_5m">At most 1 every 5 min</option>
|
||
<option value="900000" data-i18n="settings.sounds_section.popup_15m">At most 1 every 15 min</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.popup_limit_hint">Throttle how often desktop/browser notification pop-ups appear, so a burst of messages or a flaky connection can't keep popping the app up. Notification sounds and unread badges are unaffected.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.auto_accept_streams">Auto-accept Streams</span>
|
||
<input type="checkbox" id="auto-accept-streams" checked>
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.auto_accept_streams_hint">Automatically open screen shares when someone starts streaming. Turn off to join manually.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.hide_voice_panel">Hide Voice Panel</span>
|
||
<input type="checkbox" id="hide-voice-panel">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.hide_voice_panel_hint">Hide the voice users panel on desktop. Voice users are still shown in channel indicators.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.sidebar_voice_controls">Sidebar Voice Controls</span>
|
||
<input type="checkbox" id="sidebar-voice-controls">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.sidebar_voice_controls_hint">Show mute/deafen buttons in the sidebar instead of the voice panel header.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.voice_action_cues">Mute / PTT Cues</span>
|
||
<input type="checkbox" id="notif-voice-action-cues-enabled" checked>
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.voice_action_cues_hint">Play short cue tones when muting, unmuting, deafening, or using push-to-talk in Haven Desktop.</small>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.volume">Volume</span>
|
||
<input type="range" id="notif-volume" min="0" max="100" value="50" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.messages">Messages</span>
|
||
<select id="notif-msg-sound">
|
||
<option value="ping">Ping</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="bell">Bell</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="alert">Alert</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.msg_sent">Msg Sent</span>
|
||
<select id="notif-sent-sound">
|
||
<option value="swoosh">Swoosh</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div class="notif-divider"></div>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.mention_vol">@Mention Vol</span>
|
||
<input type="range" id="notif-mention-volume" min="0" max="100" value="80" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.mentions">@Mentions</span>
|
||
<select id="notif-mention-sound">
|
||
<option value="bell">Bell</option>
|
||
<option value="alert">Alert</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div class="notif-divider"></div>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.reply_vol">Reply Vol</span>
|
||
<input type="range" id="notif-reply-volume" min="0" max="100" value="80" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.replies">Replies</span>
|
||
<select id="notif-reply-sound">
|
||
<option value="chime">Chime</option>
|
||
<option value="bell">Bell</option>
|
||
<option value="alert">Alert</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div class="notif-divider"></div>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.join_vol">Join Vol</span>
|
||
<input type="range" id="notif-join-volume" min="0" max="100" value="80" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.user_joined">User Joined</span>
|
||
<select id="notif-join-sound">
|
||
<option value="chime">Chime</option>
|
||
<option value="bell">Bell</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div class="notif-divider"></div>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.leave_vol">Leave Vol</span>
|
||
<input type="range" id="notif-leave-volume" min="0" max="100" value="80" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.user_left">User Left</span>
|
||
<select id="notif-leave-sound">
|
||
<option value="drop">Drop</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<button class="btn-sm btn-full btn-accent" id="open-sound-manager-user-btn">🎵 <span data-i18n="settings.sounds_section.sound_manager_btn">Sound Manager</span></button>
|
||
<small class="settings-hint" style="display:block;margin-top:4px" data-i18n="settings.sounds_section.sound_manager_hint">Browse sounds, assign to events, set hotkeys</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Push Notifications Section -->
|
||
<div class="settings-section" id="section-push">
|
||
<h5 class="settings-section-title">📲 <span data-i18n="settings.push_section.title">Push Notifications</span></h5>
|
||
<div class="notif-settings">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.push_section.enable">Enable Push</span>
|
||
<input type="checkbox" id="push-notif-enabled">
|
||
</label>
|
||
<small class="settings-hint" id="push-notif-status" data-i18n="settings.push_section.checking">Checking...</small>
|
||
<small class="settings-hint" style="opacity:0.5" data-i18n="settings.push_section.hint">Receive notifications even when Haven is closed</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Password Change Section -->
|
||
<div class="settings-section" id="section-password">
|
||
<h5 class="settings-section-title">🔒 <span data-i18n="settings.password_section.title">Password</span></h5>
|
||
<div class="password-change-form">
|
||
<div class="form-group compact">
|
||
<input type="password" id="current-password" data-i18n-placeholder="settings.password_section.current_placeholder" placeholder="Current password" maxlength="128" autocomplete="current-password">
|
||
</div>
|
||
<div class="form-group compact">
|
||
<input type="password" id="new-password" data-i18n-placeholder="settings.password_section.new_placeholder" placeholder="New password (8+ chars)" maxlength="128" autocomplete="new-password">
|
||
</div>
|
||
<div class="form-group compact">
|
||
<input type="password" id="confirm-password" data-i18n-placeholder="settings.password_section.confirm_placeholder" placeholder="Confirm new password" maxlength="128" autocomplete="new-password">
|
||
</div>
|
||
<button class="btn-sm btn-full" id="change-password-btn" data-i18n="settings.password_section.change_btn">Change Password</button>
|
||
<small class="settings-hint" id="password-status"></small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Sessions Section -->
|
||
<div class="settings-section" id="section-sessions">
|
||
<h5 class="settings-section-title">💻 <span data-i18n="settings.sessions_section.title">Active Sessions</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.sessions_section.hint">Devices with Haven open right now. A device that is signed in but closed will not be listed, so sign out everywhere if you think someone else has your account.</small>
|
||
<div id="sessions-list" class="sessions-list"></div>
|
||
<button class="btn-sm btn-full btn-danger" id="revoke-sessions-btn" data-i18n="settings.sessions_section.revoke_btn">Sign out all other devices</button>
|
||
<small class="settings-hint" id="sessions-status"></small>
|
||
</div>
|
||
|
||
<!-- Two-Factor Authentication Section -->
|
||
<div class="settings-section" id="section-2fa">
|
||
<h5 class="settings-section-title">🔐 <span data-i18n="settings.two_factor_section.title">Two-Factor Authentication</span></h5>
|
||
|
||
<!-- Status display -->
|
||
<div id="totp-status-area">
|
||
<p class="settings-hint" id="totp-status-text"></p>
|
||
</div>
|
||
|
||
<!-- Enable flow (shown when TOTP is OFF) -->
|
||
<div id="totp-enable-area" style="display:none">
|
||
<p class="settings-hint" style="margin-bottom:10px" data-i18n="settings.two_factor_section.enable_hint">Protect your account with a TOTP authenticator app (Google Authenticator, Authy, etc.).</p>
|
||
<button class="btn-sm btn-full" id="totp-enable-btn" data-i18n="settings.two_factor_section.enable_btn">Enable Two-Factor Auth</button>
|
||
</div>
|
||
|
||
<!-- Setup flow (shown after clicking Enable) -->
|
||
<div id="totp-setup-area" style="display:none">
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.two_factor_section.setup_hint">Scan this QR code with your authenticator app:</p>
|
||
<div style="text-align:center;margin:12px 0">
|
||
<img id="totp-qr-img" data-i18n-alt="settings.two_factor_section.qr_alt" alt="QR Code" style="max-width:200px;border-radius:8px;background:#fff;padding:8px">
|
||
</div>
|
||
<p class="settings-hint" style="margin-bottom:4px" data-i18n="settings.two_factor_section.manual_hint">Or enter this secret manually:</p>
|
||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px">
|
||
<code id="totp-secret-text" style="font-size:0.875rem;letter-spacing:2px;user-select:all;flex:1;word-break:break-all"></code>
|
||
<button class="btn-sm" id="totp-copy-secret" data-i18n-title="settings.two_factor_section.copy_secret_title" title="Copy secret" style="white-space:nowrap">📋 <span data-i18n="settings.two_factor_section.copy_secret_btn">Copy</span></button>
|
||
</div>
|
||
<div class="form-group compact">
|
||
<input type="text" id="totp-verify-code" data-i18n-placeholder="settings.two_factor_section.verify_placeholder" placeholder="Enter 6-digit code to confirm" maxlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" style="text-align:center;font-size:1.125rem;letter-spacing:6px">
|
||
</div>
|
||
<button class="btn-sm btn-full" id="totp-verify-setup-btn" data-i18n="settings.two_factor_section.verify_btn">Verify & Activate</button>
|
||
<button class="btn-sm btn-full" id="totp-cancel-setup-btn" data-i18n="modals.common.cancel" style="margin-top:6px;opacity:0.7">Cancel</button>
|
||
<small class="settings-hint" id="totp-setup-status"></small>
|
||
</div>
|
||
|
||
<!-- Backup codes display (shown after successful setup) -->
|
||
<div id="totp-backup-area" style="display:none">
|
||
<h5 style="margin-bottom:6px">🗝️ <span data-i18n="settings.two_factor_section.backup_title">Backup Codes</span></h5>
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.two_factor_section.backup_hint">Save these codes somewhere safe. Each can be used once if you lose access to your authenticator.</p>
|
||
<div id="totp-backup-codes" style="font-family:monospace;font-size:0.9375rem;line-height:2;background:var(--bg-primary);padding:12px;border-radius:8px;text-align:center;user-select:all"></div>
|
||
<button class="btn-sm btn-full" id="totp-copy-backup-btn" style="margin-top:8px">📋 <span data-i18n="settings.two_factor_section.copy_backup_btn">Copy Backup Codes</span></button>
|
||
<button class="btn-sm btn-full" id="totp-backup-done-btn" data-i18n="settings.two_factor_section.done_btn" style="margin-top:6px">Done</button>
|
||
</div>
|
||
|
||
<!-- Manage area (shown when TOTP is ON) -->
|
||
<div id="totp-manage-area" style="display:none">
|
||
<p class="settings-hint" style="color:var(--accent);margin-bottom:6px">✅ <span data-i18n="settings.two_factor_section.enabled_text">Two-factor authentication is enabled</span></p>
|
||
<p class="settings-hint" id="totp-backup-remaining" style="margin-bottom:12px"></p>
|
||
<div class="form-group compact">
|
||
<input type="password" id="totp-disable-password" data-i18n-placeholder="settings.two_factor_section.disable_password_placeholder" placeholder="Enter password to modify" maxlength="128" autocomplete="current-password">
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn-sm btn-danger-fill" id="totp-disable-btn" data-i18n="settings.two_factor_section.disable_btn" style="flex:1">Disable 2FA</button>
|
||
<button class="btn-sm btn-accent" id="totp-regen-backup-btn" data-i18n="settings.two_factor_section.regen_backup_btn" style="flex:1">Regenerate Backup Codes</button>
|
||
</div>
|
||
<small class="settings-hint" id="totp-manage-status"></small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Account Recovery Codes Section -->
|
||
<div class="settings-section" id="section-recovery">
|
||
<h5 class="settings-section-title">🔑 <span data-i18n="settings.recovery_section.title">Account Recovery</span></h5>
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.recovery_section.desc">Generate one-time recovery codes to regain access if you forget your password. Store them somewhere safe — they work like a last-resort key.</p>
|
||
<div class="settings-warning-box" id="recovery-e2e-warning" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.4);border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:0.82rem;color:var(--text-secondary)">
|
||
<span data-i18n-html="settings.recovery_section.e2e_warning">⚠️ <strong>Important:</strong> Using a recovery code to reset your password will permanently and irreversibly destroy your end-to-end encryption keys. All encrypted direct message history will become unreadable. There is no way to undo this.</span>
|
||
</div>
|
||
<!-- Status -->
|
||
<p class="settings-hint" id="recovery-code-status" style="margin-bottom:10px"></p>
|
||
<!-- Generate flow -->
|
||
<div id="recovery-generate-area">
|
||
<div class="form-group compact">
|
||
<input type="password" id="recovery-gen-password" data-i18n-placeholder="settings.recovery_section.confirm_password_placeholder" placeholder="Confirm your password" maxlength="128" autocomplete="current-password">
|
||
</div>
|
||
<button class="btn-sm btn-full btn-accent" id="recovery-generate-btn" data-i18n="settings.recovery_section.generate_btn">Generate Recovery Codes</button>
|
||
<small class="settings-hint" id="recovery-gen-status"></small>
|
||
</div>
|
||
<!-- Codes display (shown after generation) -->
|
||
<div id="recovery-codes-area" style="display:none">
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.recovery_section.save_hint">Save these codes now — they will not be shown again. Each code can only be used once.</p>
|
||
<div id="recovery-codes-list" style="font-family:monospace;font-size:0.9375rem;line-height:2;background:var(--bg-primary);padding:12px;border-radius:8px;text-align:center;user-select:all"></div>
|
||
<button class="btn-sm btn-full" id="recovery-copy-btn" style="margin-top:8px">📋 <span data-i18n="settings.recovery_section.copy_codes_btn">Copy Codes</span></button>
|
||
<button class="btn-sm btn-full" id="recovery-codes-done-btn" data-i18n="settings.recovery_section.done_btn" style="margin-top:6px">Done</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Account Section -->
|
||
<div class="settings-section" id="section-delete-account">
|
||
<h5 class="settings-section-title">⚠️ <span data-i18n="settings.delete_account_section.title">Delete Account</span></h5>
|
||
<p class="settings-hint" style="margin-bottom:8px;" data-i18n="settings.delete_account_section.desc">Permanently delete your account. This cannot be undone.</p>
|
||
<button class="btn-sm btn-danger-fill" id="delete-account-btn" data-i18n="settings.delete_account_section.btn">Delete My Account</button>
|
||
</div>
|
||
|
||
<!-- Plugins & Themes Section -->
|
||
<div class="settings-section" id="section-plugins">
|
||
<h5 class="settings-section-title">🧩 <span data-i18n="settings.plugins_section.title">Plugins & Themes</span></h5>
|
||
<p class="settings-hint" data-i18n-html="settings.plugins_section.hint">Drop <code>.plugin.js</code> files into the <code>plugins/</code> folder and <code>.theme.css</code> files into the <code>themes/</code> folder, then refresh.</p>
|
||
<div id="extension-safe-mode-notice" style="display:none;margin:10px 0 12px;padding:10px;border:1px solid var(--warning);border-radius:var(--radius);background:var(--bg-tertiary)">
|
||
<strong id="extension-safe-mode-title" data-i18n="settings.plugins_section.safe_mode_title">Extension Safe Mode</strong>
|
||
<p id="extension-safe-mode-desc" class="settings-hint" style="margin:4px 0 10px" data-i18n="settings.plugins_section.safe_mode_desc">Custom themes and plugins are suppressed for this session. You can reset them or exit without changing your saved choices.</p>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn-sm btn-danger-fill" id="extension-reset-btn" data-i18n="settings.plugins_section.reset_customizations">Reset themes and plugins</button>
|
||
<button class="btn-sm" id="extension-safe-mode-exit-btn" data-i18n="settings.plugins_section.exit_safe_mode">Exit Safe Mode</button>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;margin-bottom:12px;">
|
||
<button class="btn-sm btn-accent" id="plugin-refresh-btn">🔄 <span data-i18n="settings.plugins_section.refresh_btn">Refresh</span></button>
|
||
</div>
|
||
<h6 style="margin:10px 0 6px;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted)" data-i18n="settings.plugins_section.plugins_label">Plugins</h6>
|
||
<div id="plugin-list" class="plugin-list"></div>
|
||
<h6 style="margin:14px 0 6px;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted)" data-i18n="settings.plugins_section.themes_label">Themes</h6>
|
||
<div id="theme-list" class="plugin-list"></div>
|
||
</div>
|
||
|
||
<!-- Desktop Shortcuts Section (only shown in Haven Desktop) -->
|
||
<div class="settings-section" id="section-desktop-shortcuts" style="display:none">
|
||
<h5 class="settings-section-title">⌨️ <span data-i18n="settings.desktop_shortcuts_section.title">Desktop Shortcuts</span></h5>
|
||
<p class="settings-desc" data-i18n="settings.desktop_shortcuts_section.desc">Global keyboard shortcuts — active even when Haven is in the background. Click a row to rebind, or "Clear" to disable.</p>
|
||
<div class="shortcut-table" id="shortcut-table">
|
||
<div class="shortcut-row" id="shortcut-row-mute">
|
||
<span class="shortcut-label" data-i18n="settings.desktop_shortcuts_section.mute_unmute">Mute / Unmute</span>
|
||
<span class="shortcut-key" id="shortcut-key-mute">—</span>
|
||
<div class="shortcut-btns">
|
||
<button class="btn-xs shortcut-record-btn" data-action="mute" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
|
||
<button class="btn-xs shortcut-clear-btn" data-action="mute" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div class="shortcut-row" id="shortcut-row-deafen">
|
||
<span class="shortcut-label" data-i18n="settings.desktop_shortcuts_section.deafen_undeafen">Deafen / Undeafen</span>
|
||
<span class="shortcut-key" id="shortcut-key-deafen">—</span>
|
||
<div class="shortcut-btns">
|
||
<button class="btn-xs shortcut-record-btn" data-action="deafen" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
|
||
<button class="btn-xs shortcut-clear-btn" data-action="deafen" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div class="shortcut-row" id="shortcut-row-ptt">
|
||
<span class="shortcut-label">
|
||
<span data-i18n="settings.desktop_shortcuts_section.ptt">Push-to-Talk</span>
|
||
<select id="ptt-mode-select" class="settings-select" style="font-weight:400;font-size:0.85em;margin-left:6px;padding:1px 4px" data-i18n-title="settings.desktop_shortcuts_section.ptt_mode_title" title="How the PTT key behaves">
|
||
<option value="hold" data-i18n="settings.desktop_shortcuts_section.ptt_hold">hold</option>
|
||
<option value="toggle" data-i18n="settings.desktop_shortcuts_section.ptt_toggle_mode">toggle</option>
|
||
</select>
|
||
</span>
|
||
<span class="shortcut-key" id="shortcut-key-ptt" data-i18n="settings.desktop_shortcuts_section.not_set">Not set</span>
|
||
<div class="shortcut-btns">
|
||
<button class="btn-xs shortcut-record-btn" data-action="ptt" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
|
||
<button class="btn-xs shortcut-clear-btn" data-action="ptt" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="settings-hint" id="shortcut-status" style="margin-top:8px" data-i18n="settings.desktop_shortcuts_section.binding_hint">Lone modifiers (Alt / Ctrl / Shift) and extra mouse buttons (Mouse4 / Mouse5) can be bound here. Some bindings require a recent Haven Desktop build to register at the OS level.</p>
|
||
</div>
|
||
|
||
<!-- Desktop App Settings Section (only shown in Haven Desktop) -->
|
||
<div class="settings-section" id="section-desktop-app" style="display:none">
|
||
<h5 class="settings-section-title">🖥️ <span data-i18n="settings.desktop_app_section.title">Desktop App</span></h5>
|
||
<p class="settings-desc" data-i18n="settings.desktop_app_section.desc">Settings specific to Haven Desktop.</p>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-start-on-login">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.start_on_login">Start on Login</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.start_on_login_hint">Launch Haven Desktop automatically when you log in to your computer.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row" id="pref-start-hidden-row" style="display:none">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-start-hidden">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.start_hidden">Start Hidden to Tray</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.start_hidden_hint">When launching on login, start minimized to the system tray instead of showing the window.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-minimize-to-tray">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.minimize_to_tray">Minimize to Tray on Close</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.minimize_to_tray_hint">Closing the window hides Haven to the system tray instead of quitting.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-hide-menu-bar">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.hide_menu_bar">Hide Menu Bar</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.hide_menu_bar_hint">Hide the File / Edit / View / Window / Help toolbar. Press Alt to temporarily show it.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row" style="margin-top:16px;padding-top:12px;border-top:1px solid var(--border)">
|
||
<span class="settings-hint" id="desktop-version-info" style="opacity:0.6"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Debug Section -->
|
||
<div class="settings-section" id="section-debug">
|
||
<h5 class="settings-section-title">🐞 <span data-i18n="settings.debug_section.title">Debug</span></h5>
|
||
<p class="settings-desc" data-i18n="settings.debug_section.desc">Experimental client-side toggles. These don't change anything on the server.</p>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-debug-local-talk-indicator">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.local_talk_indicator">Local mic talk indicator</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.local_talk_indicator_hint">Highlight your own avatar the moment your mic crosses the talk threshold, instead of waiting for the server to echo it back. Default off — the server echo is usually preferred because it confirms your audio actually reached the server.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-voice-mute-on-join">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.mute_on_join">Always join voice muted</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.mute_on_join_hint">Force the mic to start muted every time you join a voice channel. Useful if you tend to lurk first, or if you don't want to risk hot-mic'ing right after joining. You can unmute normally once you're in.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-voice-listener-only">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.listener_only">Join voice without microphone (listener only)</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.listener_only_hint">Join voice channels without requesting microphone access. You'll hear everyone but won't be able to talk. Useful if you don't own a mic, can't grant mic permission right now, or just want to listen in. Haven will also fall back to listener-only automatically if the mic can't be opened for any reason. (#5380)</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-debug-screen-share-voice-proc">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.screen_share_voice_proc">Apply voice processing to screen-share audio</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.screen_share_voice_proc_hint">Re-enable echo cancellation, noise suppression, and auto gain control on the audio captured from your screen share. Default off as of 3.17.3 because these filters mangle music and game audio. Turn this on if you're sharing voice content (tutorial narration, a podcast, a meeting) where the cleanup actually helps. Does not affect your microphone, which always gets full voice processing.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-debug-screen-audio-direct">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.screen_audio_webaudio">Web Audio mixing for screen-share audio</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.screen_audio_webaudio_hint">By default Haven plays incoming screen-share audio straight through the audio element, which keeps it in sync with the video over a TURN/relay connection. Turn this on to route it through Haven's Web Audio mixer instead, which unlocks the per-stream volume boost above 100% but can cause audio stutter and audio/video desync over a relay (it's fine on a local network). Leave it off if a screen share's audio stutters or drifts out of sync. (#5426)</p>
|
||
</div>
|
||
|
||
<div class="settings-group">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-debug-screen-relay-profile">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.screen_relay_profile">Gentler screen share for relay connections</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.screen_relay_profile_hint">Turn this on if your screen share stutters and drifts out of sync for viewers, but only when the call goes through a TURN server and never on your local network. It lowers the video bitrate back to the pre-3.18.1 values and lets the encoder drop frames when the connection is struggling, instead of holding the framerate and letting the backlog grow. Picture quality will be lower. Only the person sharing needs it on. (#5426)</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-debug-voice-glare-ice-fix">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.voice_glare_ice_fix">Repair voice audio after a reconnect (experimental)</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.voice_glare_ice_fix_hint">If you sometimes rejoin a voice call and still can't hear one or more people until you leave and rejoin, turn this on. When two people reconnect at the same moment their connections can collide mid-repair and one direction of audio stays dead; this makes Haven re-run the repair so the audio comes back on its own. Off by default while it's still being tested. (#5444)</p>
|
||
</div>
|
||
|
||
<!-- Desktop-only debug setting: shown when running in Haven Desktop -->
|
||
<div class="desktop-pref-row" id="pref-force-sdr-row" style="display:none">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-force-sdr">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.force_sdr">Force SDR (sRGB) Color</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.force_sdr_hint">Fix washed-out or over-saturated colors on HDR monitors. Requires restart.</p>
|
||
</div>
|
||
|
||
<!-- Desktop-only: G-Sync / VRR FPS-drop workaround (#35) -->
|
||
<div class="desktop-pref-row" id="pref-disable-gpu-vsync-row" style="display:none">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-disable-gpu-vsync">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.disable_gpu_vsync">Disable GPU vsync (Nvidia G-Sync fix)</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.disable_gpu_vsync_hint">Workaround for Nvidia G-Sync / VRR monitors where Haven Desktop drops to ~5 FPS after the window is minimized or hidden behind another window. Tells Chromium not to wait for the monitor's vsync signal before drawing frames. May cause visible tearing on monitors without variable refresh rate. Requires restart to take effect.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row" id="pref-unlimit-frame-rate-row" style="display:none">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-unlimit-frame-rate">
|
||
<span class="toggle-label" data-i18n="settings.debug_section.unlimit_frame_rate">Remove Chromium frame-rate cap (Nvidia G-Sync fix)</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.debug_section.unlimit_frame_rate_hint">Pairs with the GPU vsync toggle above. Removes Chromium's internal frame-rate limit, which on some Nvidia G-Sync setups gets stuck at a very low value and never recovers. Only useful if the vsync toggle alone didn't help. Can increase CPU/GPU usage. Requires restart to take effect.</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /settings-body-user -->
|
||
|
||
<!-- Admin settings body (separate tab) -->
|
||
<div class="settings-body" id="settings-body-admin" style="display:none">
|
||
|
||
<!-- Admin Section (hidden for non-admins) -->
|
||
<div class="settings-section admin-settings-section" id="admin-mod-panel" style="display:none">
|
||
|
||
<!-- Server Updates -->
|
||
<div class="admin-settings" id="section-update">
|
||
<h5 class="settings-section-subtitle">⬆️ <span data-i18n="settings.admin.update_title">Server Updates</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.update_hint">Check for new Haven releases and apply them in place. The server will create a pre-update backup automatically and restart itself.</small>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
|
||
<button class="btn-sm btn-accent" id="update-check-btn" data-i18n="settings.admin.update_check_btn">Check for Updates</button>
|
||
<button class="btn-sm" id="update-run-btn" disabled data-i18n="settings.admin.update_run_btn">Update Now</button>
|
||
</div>
|
||
<div id="update-status" style="margin-top:10px;padding:8px;border:1px solid var(--border);border-radius:4px;display:none"></div>
|
||
</div>
|
||
|
||
<!-- Server Branding -->
|
||
<div class="admin-settings" id="section-branding">
|
||
<h5 class="settings-section-subtitle">🏠 <span data-i18n="settings.admin.branding_title">Server Branding</span></h5>
|
||
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.server_name">Server Name</span>
|
||
<input type="text" id="server-name-input" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="HAVEN">
|
||
</label>
|
||
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.login_title">Login Title</span>
|
||
<input type="text" id="server-title-input" maxlength="40" class="settings-text-input" style="max-width:200px" data-i18n-placeholder="settings.admin.login_title_placeholder" placeholder="(shown on login page)">
|
||
</label>
|
||
<small class="settings-hint" style="margin-top:2px;display:block" data-i18n="settings.admin.login_title_hint">Custom title displayed on the login screen below the HAVEN logo.</small>
|
||
|
||
<!-- Server Icon -->
|
||
<div class="settings-group" style="margin-top:12px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.server_icon_hint">Server icon (square, max 2 MB)</small>
|
||
<div class="server-icon-upload-area">
|
||
<div class="server-icon-preview" id="server-icon-preview">
|
||
<span class="server-icon-text">⬡</span>
|
||
</div>
|
||
<div class="server-icon-upload-controls">
|
||
<input type="file" id="server-icon-file" accept="image/*" style="font-size:0.6875rem;max-width:160px">
|
||
<div style="display:flex;gap:4px;margin-top:4px">
|
||
<button class="btn-sm btn-accent" id="server-icon-upload-btn" data-i18n="settings.admin.upload_btn">Upload</button>
|
||
<button class="btn-sm" id="server-icon-remove-btn" data-i18n="settings.admin.remove_btn">Remove</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Server Banner -->
|
||
<div class="settings-group" style="margin-top:12px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.server_banner_hint">Server banner (wide, max 4 MB)</small>
|
||
<div class="server-banner-upload-area">
|
||
<div class="server-banner-preview" id="server-banner-preview">
|
||
<span class="muted-text" style="font-size:0.6875rem" data-i18n="settings.admin.no_banner">No banner</span>
|
||
</div>
|
||
<div class="server-banner-upload-controls">
|
||
<input type="file" id="server-banner-file" accept="image/jpeg,image/png,image/gif,image/webp" style="font-size:0.6875rem;max-width:160px">
|
||
<div style="display:flex;gap:4px;margin-top:4px">
|
||
<button class="btn-sm btn-accent" id="server-banner-upload-btn" data-i18n="settings.admin.upload_btn">Upload</button>
|
||
<button class="btn-sm" id="server-banner-remove-btn" data-i18n="settings.admin.remove_btn">Remove</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<small class="settings-hint" data-i18n="settings.admin.server_banner_display_hint">Banner display settings (height, offset, header style) are in User Settings.</small>
|
||
</div>
|
||
|
||
<!-- Theme & Welcome -->
|
||
<div class="settings-group" style="margin-top:12px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.appearance_welcome">Appearance & Welcome</small>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.default_theme">Default Theme</span>
|
||
<select id="default-theme-select" style="max-width:160px">
|
||
<option value="" data-i18n="settings.admin.no_theme">None (user's choice)</option>
|
||
<option value="haven">Haven</option>
|
||
<option value="discord">Discord</option>
|
||
<option value="matrix">Matrix</option>
|
||
<option value="tron">Tron</option>
|
||
<option value="halo">Halo</option>
|
||
<option value="lotr">Lord of the Rings</option>
|
||
<option value="cyberpunk">Cyberpunk</option>
|
||
<option value="nord">Nord</option>
|
||
<option value="dracula">Dracula</option>
|
||
<option value="bloodborne">Bloodborne</option>
|
||
<option value="darksouls">Dark Souls</option>
|
||
<option value="eldenring">Elden Ring</option>
|
||
<option value="ice">Ice</option>
|
||
<option value="abyss">Abyss</option>
|
||
<option value="minecraft">Minecraft</option>
|
||
<option value="ffx">Final Fantasy</option>
|
||
<option value="zelda">Zelda</option>
|
||
<option value="fallout">Fallout</option>
|
||
<option value="scripture">Scripture</option>
|
||
<option value="chapel">Chapel</option>
|
||
<option value="gospel">Gospel</option>
|
||
<option value="midnightpurple">Midnight Purple</option>
|
||
<option value="crt">CRT</option>
|
||
<option value="win95">Windows 95</option>
|
||
<option value="rgb">RGB</option>
|
||
<option value="daylight">Daylight (Light)</option>
|
||
<option value="cloudy">Cloudy (Light)</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.default_theme_hint">New users see this theme on first visit. They can still pick their own.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.default_locale">Default Language</span>
|
||
<select id="default-locale-select" style="max-width:160px">
|
||
<option value="" data-i18n="settings.admin.no_locale">Auto-detect (browser)</option>
|
||
<option value="en">English</option>
|
||
<option value="fr">Français</option>
|
||
<option value="de">Deutsch</option>
|
||
<option value="es">Español</option>
|
||
<option value="pl">Polski</option>
|
||
<option value="ru">Русский</option>
|
||
<option value="zh">中文</option>
|
||
<option value="pt">Português (Brasil)</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.default_locale_hint">New users see Haven in this language on first visit. They can still pick their own.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.welcome_message">Welcome Message</span>
|
||
<input type="text" id="welcome-message-input" maxlength="500" class="settings-text-input" style="max-width:280px" data-i18n-placeholder="settings.admin.welcome_message_placeholder" placeholder="e.g. Welcome {user}!">
|
||
</label>
|
||
<small class="settings-hint" data-i18n-html="settings.admin.welcome_message_hint">Shown when a new user joins a channel. Use <code>{user}</code> for the username. Leave blank to disable.</small>
|
||
</div>
|
||
|
||
<!-- Custom Themes -->
|
||
<div class="settings-group" style="margin-top:12px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.custom_themes_title">Custom Themes</small>
|
||
<p class="settings-hint" style="margin:0 0 8px" data-i18n-html="settings.admin.custom_themes_hint">Drop <code>.theme.css</code> files into the <code>themes/</code> folder, then check the ones you want to appear in the theme picker for all users.</p>
|
||
<div id="admin-theme-list" style="display:flex;flex-direction:column;gap:6px">
|
||
<span style="font-size:0.75rem;color:var(--text-muted)" data-i18n-html="settings.admin.no_themes">No themes found. Add <code>.theme.css</code> files to the <code>themes/</code> folder and refresh.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-settings" id="section-members" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.show_members">Show Members</span>
|
||
<select id="member-visibility-select">
|
||
<option value="online" data-i18n="settings.admin.members_online_only">Online Only</option>
|
||
<option value="all" data-i18n="settings.admin.members_all">All Members</option>
|
||
<option value="none" data-i18n="settings.admin.members_hidden">Hidden</option>
|
||
</select>
|
||
</label>
|
||
<div class="settings-btn-grid">
|
||
<button class="btn-sm btn-accent" id="view-bans-btn">📋 <span data-i18n="settings.admin.view_bans_btn">View Bans</span></button>
|
||
<button class="btn-sm btn-accent" id="view-ip-bans-btn">🚫 <span data-i18n="settings.admin.view_ip_bans_btn">View IP Bans</span></button>
|
||
<button class="btn-sm btn-accent" id="view-deleted-users-btn">🗑️ <span data-i18n="settings.admin.view_deleted_btn">View Deleted Users</span></button>
|
||
<button class="btn-sm btn-accent" id="view-all-members-btn">👥 <span data-i18n="settings.admin.view_all_members_btn">View All Members</span></button>
|
||
</div>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.update_banner_label">Update banner (admins only)</span>
|
||
<input type="checkbox" id="update-banner-admin-only">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.update_banner_hint">When enabled, the "update available" banner is only shown to admins</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.admin_pw_reset_label">Allow admin password reset</span>
|
||
<input type="checkbox" id="admin-password-reset-enabled">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.admin_pw_reset_hint">⚠️ When enabled, admins can reset any user's password to a one-time temp value. The user must change it on next login. Resetting a password also wipes that user's E2E DM history (their wrap key derives from the password). The fact that this is enabled is exposed via /api/public-config so users can see whether admins on this server have this power.</small>
|
||
</div>
|
||
<div class="admin-settings" id="section-moderation" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🛡️ <span data-i18n="settings.admin.moderation_title">Moderation</span></h5>
|
||
|
||
<!-- Idle-online oversight (v3.46.0) -->
|
||
<small class="settings-group-label" style="margin-top:8px;display:block">🕵️ <span data-i18n="settings.admin.idle_online_group">Idle-online accounts</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.idle_online_desc">Lists accounts that have been connected and showing as online for hours without posting, joining voice, or changing their status. A normal client goes idle on its own, so an account that stays green and silent for this long is often a script parked to watch the server rather than a person. This is just something to look at, not an automatic action.</small>
|
||
<div class="settings-btn-grid" style="margin-top:8px;align-items:center">
|
||
<label class="select-row" style="margin:0">
|
||
<span data-i18n="settings.admin.idle_online_threshold">Flag after (hours)</span>
|
||
<input type="number" id="idle-online-hours" min="1" max="168" step="1" value="4" style="max-width:90px">
|
||
</label>
|
||
<button class="btn-sm btn-accent" id="idle-online-refresh">🔄 <span data-i18n="settings.admin.idle_online_check">Check now</span></button>
|
||
</div>
|
||
<div id="idle-online-list" class="whitelist-list" style="margin-top:8px"></div>
|
||
</div>
|
||
|
||
<div class="admin-settings" id="section-security" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🔒 <span data-i18n="settings.admin.security_title">Security</span></h5>
|
||
|
||
<!-- (#12) SSO / OIDC -->
|
||
<small class="settings-group-label" style="margin-top:8px;display:block">🔑 <span data-i18n="settings.admin.oidc_group">Single sign-on (OIDC)</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n-html="settings.admin.oidc_desc">Let members sign in with an account from your own identity provider (Authentik, Keycloak, Authelia, Auth0 and anything else that speaks OpenID Connect) instead of a Haven password. Fill in the issuer address and client ID here, then put the client secret in your .env file as OIDC_CLIENT_SECRET and restart. The secret is deliberately kept out of the database so your backups never carry it. Register this address with your provider as the redirect URI: <code>https://your-haven-address/api/auth/oidc/callback</code></small>
|
||
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.oidc_e2e_note">Encrypted DMs are unlocked by a key derived from what you type when you sign in, and an SSO member never types a password here. They are asked to set a separate encryption passphrase on their first sign-in, and for it again on each new device. Nobody, including you, can reset that passphrase for them.</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.oidc_enabled_label">Enable SSO sign-in</span>
|
||
<input type="checkbox" id="oidc-enabled">
|
||
</label>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.oidc_issuer_label">Issuer URL</span>
|
||
<input type="text" id="oidc-issuer-url" placeholder="https://auth.example.com" spellcheck="false" autocomplete="off">
|
||
</label>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.oidc_client_id_label">Client ID</span>
|
||
<input type="text" id="oidc-client-id" spellcheck="false" autocomplete="off">
|
||
</label>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.oidc_scopes_label">Scopes</span>
|
||
<input type="text" id="oidc-scopes" placeholder="openid profile email" spellcheck="false" autocomplete="off">
|
||
</label>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.oidc_button_label_label">Button text</span>
|
||
<input type="text" id="oidc-button-label" data-i18n-placeholder="settings.admin.oidc_button_placeholder" placeholder="Sign in with SSO" spellcheck="false" autocomplete="off">
|
||
</label>
|
||
<label class="toggle-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.oidc_create_users_label">Create a Haven account on first sign-in</span>
|
||
<input type="checkbox" id="oidc-create-users" checked>
|
||
</label>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.oidc_create_users_hint">Off means only people who already have a Haven account linked to your provider can sign in this way. Since linking an existing account is not built yet, leaving this off will turn away everyone new.</small>
|
||
<div id="oidc-secret-warning" class="settings-hint" style="display:none;margin-top:6px;color:var(--warning,#e0a800)" data-i18n="settings.admin.oidc_secret_warning">OIDC_CLIENT_SECRET is not set in the server environment, so the sign-in button stays hidden until it is.</div>
|
||
|
||
<small class="settings-group-label" style="margin-top:8px;display:block">🔗 <span data-i18n="settings.admin.referrer_policy_group">Referrer configuration</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.referrer_policy_desc">Sets the Referrer-Policy header Haven sends to browsers, which controls how much of the current page's address is shared when the browser loads resources or follows links to other sites. The recommended options cover almost every server; the advanced policies are standard browser values left to your discretion. Policies that would send the full page address to other sites are not offered, because Haven invite links live in that address.</small>
|
||
<ul class="settings-hint" style="margin:6px 0 0;padding-left:20px">
|
||
<li data-i18n-html="settings.admin.referrer_policy_opt_same"><strong>Same-Origin (default):</strong> within this server, shares the full address; to other sites, shares nothing.</li>
|
||
<li style="margin-top:4px" data-i18n-html="settings.admin.referrer_policy_opt_none"><strong>No-Referrer:</strong> shares nothing anywhere — not even within this server.</li>
|
||
<li style="margin-top:4px" data-i18n-html="settings.admin.referrer_policy_opt_default"><strong>Strict-Origin-When-Cross-Origin:</strong> within this server, shares the full address; to other sites, shares only the origin. This was Haven's default before 3.41.0 — pick it to go back to the old behaviour.</li>
|
||
</ul>
|
||
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.referrer_policy_note">Same-Origin and No-Referrer send nothing to other sites, so either one fixes embeds whose CDN rejects a cross-origin referrer, such as Twitter/X video. If an image or embed somewhere else stops loading after this change, that host wants a referrer: switch to Strict-Origin-When-Cross-Origin to restore the pre-3.41.0 behaviour.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.referrer_policy_label">Referrer policy</span>
|
||
<select id="referrer-policy-select">
|
||
<optgroup label="Recommended" data-i18n-label="settings.admin.referrer_recommended">
|
||
<option value="same-origin" data-i18n="settings.admin.referrer_same_origin">Same-Origin (default)</option>
|
||
<option value="no-referrer" data-i18n="settings.admin.referrer_no_referrer">No-Referrer</option>
|
||
<option value="strict-origin-when-cross-origin" data-i18n="settings.admin.referrer_strict_cross_origin">Strict-Origin-When-Cross-Origin (Haven's pre-3.41.0 default)</option>
|
||
</optgroup>
|
||
<optgroup label="Advanced — use at your own discretion" data-i18n-label="settings.admin.referrer_advanced">
|
||
<option value="strict-origin">Strict-Origin</option>
|
||
<option value="origin">Origin</option>
|
||
<option value="origin-when-cross-origin">Origin-When-Cross-Origin</option>
|
||
</optgroup>
|
||
</select>
|
||
</label>
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">🖼️ <span data-i18n="settings.admin.media_proxy_group">Image privacy</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.media_proxy_desc">Haven fetches images from other sites itself and serves them from its own cache, so your members' browsers never contact those sites. With this off, anyone who posts an image link collects the IP address and browser details of everyone who scrolls past it, without a single click. It also means embeds keep working after the original link expires or the site goes down. Costs some bandwidth and disk on your server; each image is fetched once and cached for 30 days.</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.media_proxy_label">Proxy and cache remote images</span>
|
||
<input type="checkbox" id="media-proxy-enabled">
|
||
</label>
|
||
<div id="media-cache-stats" class="settings-hint" style="margin-top:4px"></div>
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">🎙️ <span data-i18n="settings.admin.voice_privacy_group">Voice privacy</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.voice_relay_desc">Haven voice is peer-to-peer, so by default everyone in a call can see everyone else's IP address. It is how the connection is made, it needs no clicking, and nobody is told it happened. Relay-only mode routes all voice through your TURN server instead, so participants only ever see the TURN server's address. This needs a TURN server configured under Voice & Connectivity, and it uses your server's bandwidth for every call.</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.voice_relay_label">Relay-only voice (hide member IPs)</span>
|
||
<input type="checkbox" id="voice-force-relay">
|
||
</label>
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">🔔 <span data-i18n="settings.admin.fcm_privacy_group">FCM Privacy</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n-html="settings.admin.fcm_desc">FCM (Firebase Cloud Messaging) is Google's system for delivering push notifications to Android phones. It's what wakes your phone when you receive a new message while Haven is closed. This means that all notifications (including your Haven username, channel name and message content) sent to your Android phone have to pass through Google's servers to reach your phone.<br><br>While this may be fine for most users, some may prefer to keep Google completely out of the equation. <a href="https://unifiedpush.org" target="_blank" rel="noopener">UnifiedPush</a> is a decentralized notification standard that supports several notification apps, and if a UnifiedPush compatible app is installed on your phone, Haven Mobile will prioritize it over FCM.</small>
|
||
<small class="settings-hint" style="display:block;margin-top:8px" data-i18n-html="settings.admin.fcm_note"><strong>Dangerous Action Warning: Before disabling this, please ensure every member in your Haven server including yourself has a UnifiedPush compatible app installed on their phone. Otherwise, some members of your server will not receive any mobile notifications.</strong></small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.fcm_enabled_label">Enable Google FCM Push Notifications</span>
|
||
<input type="checkbox" id="fcm-enabled">
|
||
</label>
|
||
</div>
|
||
<div class="admin-settings" id="section-automod" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🛡️ <span data-i18n="settings.admin.automod_title">Auto-Mod</span></h5>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.automod_desc">Checks the links in messages, edits, DMs, profiles and channel topics against a domain policy you control. Blocked content is rejected before it is saved, so it never reaches anyone else. This matters more than it sounds: Haven renders linked images and link previews directly from the other site, so a hostile link exposes the IP address of everyone who scrolls past it, not just whoever clicks.</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.automod_enabled">Enabled</span>
|
||
<input type="checkbox" id="automod-enabled">
|
||
</label>
|
||
<div id="automod-body" style="margin-top:8px">
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.automod_mode_label">Link policy</span>
|
||
<select id="automod-link-mode">
|
||
<option value="off" data-i18n="settings.admin.automod_mode_off">Off — allow every link</option>
|
||
<option value="allowlist" data-i18n="settings.admin.automod_mode_allowlist">Allowlist — block everything not listed</option>
|
||
<option value="blocklist" data-i18n="settings.admin.automod_mode_blocklist">Blocklist — allow everything except listed</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.automod_mode_hint">Allowlist is the strict option and the one that stops links you have never seen before. Blocklist only stops domains you already know about.</small>
|
||
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">🌐 <span data-i18n="settings.admin.automod_domains_group">Domains</span></small>
|
||
<div class="whitelist-add-row" style="margin-top:6px">
|
||
<input type="text" id="automod-domain-input" placeholder="example.com" maxlength="253">
|
||
<select id="automod-domain-mode" style="max-width:110px">
|
||
<option value="allow" data-i18n="settings.admin.automod_allow">Allow</option>
|
||
<option value="deny" data-i18n="settings.admin.automod_block">Block</option>
|
||
</select>
|
||
<button class="btn-sm btn-accent" id="automod-domain-add" data-i18n="settings.admin.automod_add">Add</button>
|
||
</div>
|
||
<label class="toggle-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.automod_include_subs">Include subdomains</span>
|
||
<input type="checkbox" id="automod-include-subdomains" checked>
|
||
</label>
|
||
<div id="automod-domain-list" class="whitelist-list" style="margin-top:8px"></div>
|
||
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">⚙️ <span data-i18n="settings.admin.automod_rules_group">Rules</span></small>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.automod_min_age">New accounts can't post links for (hours)</span>
|
||
<input type="number" id="automod-min-account-hours" min="0" max="8760" step="1" style="max-width:90px">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.automod_min_age_hint">0 disables this. Applies even to allowed domains, and is the single most effective rule against register-post-link-get-banned-repeat.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.automod_exempt_level">Skip filtering at role level</span>
|
||
<input type="number" id="automod-exempt-level" min="0" max="100" step="1" style="max-width:90px">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.automod_exempt_hint">Members at or above this level are never filtered. Admins are always exempt.</small>
|
||
<label class="toggle-row" style="margin-top:8px"><span data-i18n="settings.admin.automod_scan_edits">Check message edits</span><input type="checkbox" id="automod-scan-edits"></label>
|
||
<label class="toggle-row"><span data-i18n="settings.admin.automod_scan_dms">Check direct messages</span><input type="checkbox" id="automod-scan-dms"></label>
|
||
<small class="settings-hint" data-i18n="settings.admin.automod_scan_dms_hint">Direct messages are end-to-end encrypted, so the server genuinely cannot read them and cannot block a DM as it is sent. Instead the check runs in the recipient's app after it decrypts: a link to a domain you have not allowed is shown as plain text with a warning rather than something clickable, and images from those domains are not loaded. Senders are warned before the message goes out. This relies on the recipient running an app that implements it, so treat it as a strong deterrent rather than a guarantee.</small>
|
||
<label class="toggle-row"><span data-i18n="settings.admin.automod_scan_profile">Check profiles and channel topics</span><input type="checkbox" id="automod-scan-profile"></label>
|
||
<label class="toggle-row"><span data-i18n="settings.admin.automod_block_ip_urls">Block links to raw IP addresses</span><input type="checkbox" id="automod-block-ip-urls"></label>
|
||
<label class="toggle-row"><span data-i18n="settings.admin.automod_block_punycode">Block lookalike (internationalized) domains</span><input type="checkbox" id="automod-block-punycode"></label>
|
||
<label class="toggle-row"><span data-i18n="settings.admin.automod_block_obfuscated">Block disguised links (hxxp://, evil[.]com)</span><input type="checkbox" id="automod-block-obfuscated"></label>
|
||
<label class="toggle-row"><span data-i18n="settings.admin.automod_preview_allowlist">Only preview allowed domains</span><input type="checkbox" id="automod-preview-allowlist-only"></label>
|
||
<small class="settings-hint" data-i18n="settings.admin.automod_preview_hint">Strongly recommended. Link previews load images straight from the other site in every viewer's browser, which hands that site everyone's IP address without a single click.</small>
|
||
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">⚖️ <span data-i18n="settings.admin.automod_escalation_group">Escalation</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.automod_escalation_hint">Counts blocked attempts within a rolling window. Set any threshold to 0 to disable that step. Note that a muted member cannot send anything at all, so they stop collecting strikes until the mute runs out — if you want a fast automatic ban, set the ban threshold close to the mute threshold, or turn muting off.</small>
|
||
<label class="select-row" style="margin-top:6px"><span data-i18n="settings.admin.automod_window">Window (hours)</span><input type="number" id="automod-window-hours" min="1" max="8760" step="1" style="max-width:90px"></label>
|
||
<label class="select-row"><span data-i18n="settings.admin.automod_warn_at">Warn after</span><input type="number" id="automod-warn-at" min="0" max="1000" step="1" style="max-width:90px"></label>
|
||
<label class="select-row"><span data-i18n="settings.admin.automod_mute_at">Mute after</span><input type="number" id="automod-mute-at" min="0" max="1000" step="1" style="max-width:90px"></label>
|
||
<label class="select-row"><span data-i18n="settings.admin.automod_mute_mins">Mute length (minutes)</span><input type="number" id="automod-mute-minutes" min="1" max="43200" step="1" style="max-width:90px"></label>
|
||
<label class="select-row"><span data-i18n="settings.admin.automod_ban_at">Ban after</span><input type="number" id="automod-ban-at" min="0" max="1000" step="1" style="max-width:90px"></label>
|
||
<label class="toggle-row" style="margin-top:6px"><span data-i18n="settings.admin.automod_ban_ip">Auto-bans also ban the IP</span><input type="checkbox" id="automod-ban-ip"></label>
|
||
<small class="settings-hint" data-i18n="settings.admin.automod_ban_ip_hint">⚠️ Leave off unless you understand the risk. Shared and mobile-carrier addresses mean an IP ban can catch people who did nothing.</small>
|
||
<label class="select-row" style="margin-top:8px"><span data-i18n="settings.admin.automod_log_channel">Log to channel</span><input type="text" id="automod-log-channel" data-i18n-placeholder="settings.admin.automod_log_channel_placeholder" placeholder="channel code (optional)" maxlength="8" style="max-width:130px"></label>
|
||
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">📊 <span data-i18n="settings.admin.automod_activity_group">Recent activity</span></small>
|
||
<div class="settings-btn-grid" style="margin-top:6px">
|
||
<button class="btn-sm btn-accent" id="automod-refresh-log">🔄 <span data-i18n="settings.admin.automod_refresh">Refresh</span></button>
|
||
</div>
|
||
<div id="automod-top-hosts" style="margin-top:8px"></div>
|
||
<div id="automod-log-list" class="whitelist-list" style="margin-top:8px"></div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-whitelist" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🛡️ <span data-i18n="settings.admin.whitelist_title">Whitelist</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.whitelist_enabled">Enabled</span>
|
||
<input type="checkbox" id="whitelist-enabled">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.whitelist_hint">When enabled, only pre-approved usernames can register</small>
|
||
<div id="whitelist-panel" style="margin-top:8px;">
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="whitelist-username-input" data-i18n-placeholder="settings.admin.whitelist_username_placeholder" placeholder="Username..." maxlength="20" class="settings-text-input">
|
||
<button class="btn-sm btn-accent" id="whitelist-add-btn" data-i18n="settings.admin.whitelist_add_btn">Add</button>
|
||
</div>
|
||
<div id="whitelist-list" class="whitelist-list">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
</div>
|
||
<!-- (#5344) Registration token — independent of whitelist; can be used together or alone -->
|
||
<div style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.registration_token_required">🔑 Require invite token</span>
|
||
<input type="checkbox" id="registration-token-enabled">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.registration_token_hint">When enabled, anyone registering must enter the token below. Works alongside the whitelist (both checks must pass) or on its own.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.registration_token_invite_bypass">Allow invitation links to bypass token requirement</span>
|
||
<input type="checkbox" id="invites-bypass-registration-token">
|
||
</label>
|
||
<div id="registration-token-display" style="display:flex;align-items:center;gap:6px;margin-top:6px;">
|
||
<code id="registration-token-value" style="font-family:monospace;font-size:1rem;letter-spacing:0.05em;opacity:0.7">—</code>
|
||
<button class="btn-sm" id="copy-registration-token-btn" data-i18n-title="settings.admin.registration_token_copy" title="Copy token">📋</button>
|
||
</div>
|
||
<div style="display:flex;gap:4px;margin-top:4px;">
|
||
<button class="btn-sm btn-accent" id="generate-registration-token-btn" data-i18n="settings.admin.registration_token_generate">Generate / Reroll</button>
|
||
<button class="btn-sm" id="clear-registration-token-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<!-- Opt-in Cloudflare Turnstile CAPTCHA on registration -->
|
||
<div style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.captcha_required">🛡️ Require CAPTCHA on registration</span>
|
||
<input type="checkbox" id="registration-captcha-enabled">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.captcha_hint">Adds a Cloudflare Turnstile challenge to the sign-up form to block automated mass registration. It's free: create a widget at the Cloudflare Turnstile dashboard and paste both keys below. Leave off if you don't need it.</small>
|
||
<div class="form-group" style="margin-top:8px;">
|
||
<label for="turnstile-site-key" style="font-size:0.85rem;" data-i18n="settings.admin.captcha_site_key">Turnstile site key</label>
|
||
<input type="text" id="turnstile-site-key" class="settings-text-input" autocomplete="off" placeholder="0x4AAAAAAA..." maxlength="200">
|
||
</div>
|
||
<div class="form-group" style="margin-top:6px;">
|
||
<label for="turnstile-secret-key" style="font-size:0.85rem;" data-i18n="settings.admin.captcha_secret_key">Turnstile secret key</label>
|
||
<input type="password" id="turnstile-secret-key" class="settings-text-input" autocomplete="off" data-i18n-placeholder="settings.admin.captcha_secret_placeholder" placeholder="Kept private on the server" maxlength="200">
|
||
</div>
|
||
</div>
|
||
<!-- Opt-in global registration rate limit -->
|
||
<div style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.registration_rate_limit">⏱️ Limit new sign-ups per hour</span>
|
||
<input type="checkbox" id="registration-rate-limit-enabled">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.registration_rate_limit_hint">Caps how many new accounts can be created across the whole server each hour, to slow a bot flood even when it comes from many different addresses. Sign-ups above the cap are asked to try again later, so set it comfortably above your normal rate.</small>
|
||
<div class="form-group" style="margin-top:8px;display:flex;align-items:center;gap:8px;">
|
||
<label for="registration-rate-limit-per-hour" style="font-size:0.85rem;" data-i18n="settings.admin.registration_rate_limit_max">Max new accounts per hour</label>
|
||
<input type="number" id="registration-rate-limit-per-hour" class="settings-number-input" min="1" max="100000" value="20" style="width:90px">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-invite" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle invite-server-config">🌐 <span data-i18n="settings.admin.invite_title">Server Invite Code</span></h5>
|
||
<small class="settings-hint invite-server-config" data-i18n="settings.admin.invite_hint">A single code that adds people to every channel & sub-channel on the server</small>
|
||
<div class="invite-server-config" style="margin-top:8px;">
|
||
<div id="server-code-display" style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
||
<code id="server-code-value" style="font-family:monospace;font-size:1rem;letter-spacing:0.05em;opacity:0.7">—</code>
|
||
<button class="btn-sm" id="copy-server-code-btn" data-i18n-title="settings.admin.copy_code_title" title="Copy code">📋</button>
|
||
</div>
|
||
<div style="display:flex;gap:4px;">
|
||
<button class="btn-sm btn-accent" id="generate-server-code-btn" data-i18n="settings.admin.invite_generate_btn">Generate</button>
|
||
<button class="btn-sm" id="clear-server-code-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div class="invite-server-config" style="margin-top:12px;">
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.vanity_title">Vanity Invite Link</span>
|
||
<input type="text" id="vanity-code-input" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="my-server">
|
||
</label>
|
||
<small class="settings-hint" data-i18n-html="settings.admin.vanity_hint">Custom invite slug (3-32 chars, letters, numbers, hyphens, underscores). People can join via <code>/invite/your-slug</code></small>
|
||
<div style="display:flex;gap:4px;margin-top:4px">
|
||
<button class="btn-sm btn-accent" id="vanity-code-save-btn" data-i18n="modals.common.save">Save</button>
|
||
<button class="btn-sm" id="vanity-code-clear-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<!-- (#5345) Default channels for invite-code joiners.
|
||
Private channels are always excluded; this lets the admin
|
||
narrow it further to a curated set of public channels. -->
|
||
<div class="invite-server-config" style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
|
||
<h6 style="margin:0 0 4px;font-size:0.85rem;font-weight:600" data-i18n="settings.admin.default_join_title">📋 Default channels for invite joiners</h6>
|
||
<small class="settings-hint" data-i18n-html="settings.admin.default_join_hint">Pick which public channels people land in when they use the invite code or vanity link. Private channels are <strong>never</strong> auto-joined regardless of selection. Leave everything checked to grant all public channels.</small>
|
||
<div id="default-join-channels-list" style="margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px">
|
||
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.loading_channels">Loading channels...</p>
|
||
</div>
|
||
<div style="display:flex;gap:4px;margin-top:6px">
|
||
<button class="btn-sm" id="default-join-channels-all-btn" type="button" data-i18n="settings.admin.invite_links.select_all">Select all</button>
|
||
<button class="btn-sm" id="default-join-channels-none-btn" type="button" data-i18n="settings.admin.invite_links.select_none">Select none</button>
|
||
<button class="btn-sm btn-accent" id="default-join-channels-save-btn" type="button" data-i18n="modals.common.save">Save</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Managed invite links live in their own popout (each card is large,
|
||
so keep the settings panel uncluttered). -->
|
||
<div id="invite-links-block">
|
||
<h6 style="margin:0 0 4px;font-size:0.85rem;font-weight:600" data-i18n="settings.admin.invite_links_title">🔗 Invite Links</h6>
|
||
<small class="settings-hint" data-i18n="settings.admin.invite_links_hint">Shareable links that drop people straight into the channels you pick — no code to type. Each link can be turned off, limited to a number of people, or set to expire.</small>
|
||
<button class="btn-sm btn-accent" id="open-invite-links-btn" type="button" style="margin-top:8px">🔗 <span data-i18n="settings.admin.invite_links_manage">Manage Invite Links</span> <span id="invite-links-count" class="muted-text" style="font-weight:normal"></span></button>
|
||
</div>
|
||
<div id="invite-links-settings-block" class="invite-server-config">
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_invite_uses">Max invite link uses</span>
|
||
<input type="number" id="max-invite-uses" min="0" max="100000" step="1" style="max-width:90px">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_invite_uses_hint">The maximum number of uses an invite link can be set to. Admin and manage-server permission ignore this limit. (0 = unlimited)</small>
|
||
</div>
|
||
</div>
|
||
<!-- (#5381) Guest mode — password-less drop-in access scoped to a whitelist of channels -->
|
||
<div class="admin-settings" id="section-guests" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle" data-i18n="settings.admin.guests_title">👤 Join as Guest</h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.guests_hint">Show a "Join as Guest" button on the login page. Guests pick a username, get a temporary account with no password, and can only see the channels you whitelist below. Guests cannot DM, and their account is deleted when they disconnect (username freed).</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.guests_allow">Allow guests</span>
|
||
<input type="checkbox" id="guests-enabled">
|
||
</label>
|
||
<div style="margin-top:10px;">
|
||
<h6 style="margin:0 0 4px;font-size:0.85rem;font-weight:600" data-i18n="settings.admin.guests_channels_title">📋 Channels guests can access</h6>
|
||
<small class="settings-hint" data-i18n="settings.admin.guests_channels_hint">Pick which channels guests can see and post in. Pick none and the Guest button won't be very useful. DMs are never available to guests.</small>
|
||
<div id="guest-channels-list" style="margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px">
|
||
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.loading_channels">Loading channels...</p>
|
||
</div>
|
||
<div style="display:flex;gap:4px;margin-top:6px">
|
||
<button class="btn-sm" id="guest-channels-all-btn" type="button" data-i18n="settings.admin.invite_links.select_all">Select all</button>
|
||
<button class="btn-sm" id="guest-channels-none-btn" type="button" data-i18n="settings.admin.invite_links.select_none">Select none</button>
|
||
<button class="btn-sm btn-accent" id="guest-channels-save-btn" type="button" data-i18n="modals.common.save">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-cleanup" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🗑️ <span data-i18n="settings.admin.cleanup_title">Auto-Cleanup</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.cleanup_enabled">Enabled</span>
|
||
<input type="checkbox" id="cleanup-enabled">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.cleanup_max_age">Max Age (days)</span>
|
||
<input type="number" id="cleanup-max-age" min="0" max="3650" value="0" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.cleanup_age_hint">Delete messages older than N days (0 = off)</small>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.cleanup_max_size">Max DB Size (MB)</span>
|
||
<input type="number" id="cleanup-max-size" min="0" max="100000" value="0" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.cleanup_size_hint">Trim oldest messages when DB exceeds N MB (0 = off)</small>
|
||
<button class="btn-sm btn-full btn-accent" id="run-cleanup-now-btn" style="margin-top:4px;">🧹 <span data-i18n="settings.admin.cleanup_run_btn">Run Cleanup Now</span></button>
|
||
</div>
|
||
<div class="admin-settings" id="section-backup" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">💾 <span data-i18n="settings.admin.backup_title">Server Backup</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.backup_hint">Download a backup of this server, or restore from a previous backup. Useful when you don't have shell access to the host machine.</small>
|
||
<div class="settings-group" style="margin-top:10px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.backup_download_label">Download Backup</small>
|
||
<div class="backup-checkboxes" style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
|
||
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="channels" checked> <span data-i18n="settings.admin.backup_inc_channels">Channels & roles</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="users" checked> <span data-i18n="settings.admin.backup_inc_users">Users (no passwords / 2FA / emails)</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="settings" checked> <span data-i18n="settings.admin.backup_inc_settings">Server settings</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="messages"> <span data-i18n="settings.admin.backup_inc_messages">Messages (chat history)</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="dms"> <span data-i18n="settings.admin.backup_inc_dms">Direct messages</span> <small class="settings-hint" style="margin-left:6px;opacity:.75" data-i18n="settings.admin.backup_inc_dms_hint">(E2E ciphertext; only original key holders can read)</small></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="files"> <span data-i18n="settings.admin.backup_inc_files">Uploaded files</span></label>
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
|
||
<button class="btn-sm btn-accent" id="backup-download-btn">⬇️ <span data-i18n="settings.admin.backup_download_btn">Download Backup</span></button>
|
||
<button class="btn-sm" id="backup-select-all-btn" type="button" data-i18n="settings.admin.backup_select_all">Select All</button>
|
||
<button class="btn-sm" id="backup-select-none-btn" type="button" data-i18n="settings.admin.backup_select_none">Clear</button>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.backup_modes_hint">Pick what to include. Messages and uploaded files can make the backup very large; channels / users / settings alone are usually small. User passwords, 2FA secrets, and emails are never exported.</small>
|
||
</div>
|
||
<div class="settings-group" style="margin-top:14px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.backup_restore_label">Restore From Backup</small>
|
||
<div style="display:flex;gap:6px;align-items:center;margin-top:4px;flex-wrap:wrap">
|
||
<input type="file" id="backup-restore-file" accept=".zip" style="flex:1 1 200px;min-width:0;max-width:100%">
|
||
<button class="btn-sm btn-accent" id="backup-restore-btn">⬆️ <span data-i18n="settings.admin.backup_restore_btn">Restore</span></button>
|
||
</div>
|
||
<div id="restore-progress" class="restore-progress" style="display:none">
|
||
<div class="restore-progress-track"><div class="restore-progress-fill" id="restore-progress-fill"></div></div>
|
||
<small class="settings-hint restore-progress-label" id="restore-progress-label" data-i18n="header.uploading">Uploading…</small>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.backup_restore_full_note">Only full backups can be restored here, which means the backup must have had both Messages and Uploaded files checked when it was made. Smaller channels/users/settings backups have to be re-imported by hand.</small>
|
||
<small class="settings-hint" style="margin-top:6px;display:block;color:var(--danger,#e84a4a)" data-i18n-html="settings.admin.backup_restore_warning"><strong>Warning:</strong> Restoring overwrites the current server data. The server will restart automatically — your hosting setup must support auto-restart (Docker, systemd, the Haven installer service, etc.). The previous database is preserved as <code>haven.db.pre-restore</code> for one cycle.</small>
|
||
</div>
|
||
<div class="settings-group" style="margin-top:14px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.auto_backup_label">Automatic Backups</small>
|
||
<label class="toggle-row" style="margin-top:4px">
|
||
<span data-i18n="settings.admin.auto_backup_enable">Enable scheduled backups</span>
|
||
<input type="checkbox" id="auto-backup-enabled">
|
||
</label>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.auto_backup_interval">Interval</span>
|
||
<select id="auto-backup-interval" class="settings-number-input">
|
||
<option value="6" data-i18n="settings.admin.auto_backup_every_6_hours">Every 6 hours</option>
|
||
<option value="12" data-i18n="settings.admin.auto_backup_every_12_hours">Every 12 hours</option>
|
||
<option value="24" selected data-i18n="settings.admin.auto_backup_daily">Daily</option>
|
||
<option value="168" data-i18n="settings.admin.auto_backup_weekly">Weekly</option>
|
||
<option value="720" data-i18n="settings.admin.auto_backup_monthly">Monthly (~30d)</option>
|
||
</select>
|
||
</label>
|
||
<label class="select-row" style="margin-top:6px">
|
||
<span data-i18n="settings.admin.auto_backup_retention">Keep last N backups</span>
|
||
<input type="number" id="auto-backup-retention" min="1" max="50" value="7" class="settings-number-input">
|
||
</label>
|
||
<div class="backup-checkboxes" style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
|
||
<small class="settings-hint" data-i18n="settings.admin.auto_backup_sections_label">Include in scheduled backups:</small>
|
||
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="channels" checked> <span data-i18n="settings.admin.backup_inc_channels">Channels & roles</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="users" checked> <span data-i18n="settings.admin.auto_backup_inc_users">Users</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="settings" checked> <span data-i18n="settings.admin.backup_inc_settings">Server settings</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="messages" checked> <span data-i18n="settings.admin.auto_backup_inc_messages">Messages</span></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="dms"> <span data-i18n="settings.admin.backup_inc_dms">Direct messages</span> <small class="settings-hint" style="margin-left:6px;opacity:.75" data-i18n="settings.admin.auto_backup_inc_dms_hint">(E2E ciphertext)</small></label>
|
||
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="files"> <span data-i18n="settings.admin.backup_inc_files">Uploaded files</span></label>
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
|
||
<button class="btn-sm btn-accent" id="auto-backup-save-btn" data-i18n="settings.admin.auto_backup_save">Save Schedule</button>
|
||
<button class="btn-sm btn-accent" id="auto-backup-run-now-btn" data-i18n="settings.admin.auto_backup_run_now">Run Now</button>
|
||
<button class="btn-sm btn-accent" id="auto-backup-refresh-btn" data-i18n="settings.admin.auto_backup_refresh">Refresh List</button>
|
||
</div>
|
||
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.auto_backup_hint">Auto-backups are saved to the server's data directory and pruned to your retention count. They use the same format as manual downloads, so you can restore any of them above.</small>
|
||
<div id="auto-backup-list" style="margin-top:10px;display:flex;flex-direction:column;gap:4px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-settings" id="section-uploads" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">📁 <span data-i18n="settings.admin.uploads_title">Uploads & Limits</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.max_upload">Max Upload Size (MB)</span>
|
||
<input type="number" id="max-upload-mb" min="1" max="102400" value="25" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_upload_hint">Maximum file size users can upload in MB (default 25). No hard cap — limited by your server's available disk space.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_message_chars">Max Message Length (characters)</span>
|
||
<input type="number" id="max-message-chars" min="200" max="100000" value="2000" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_message_chars_hint">Maximum characters per message, post, or DM (200–100000, default 2000)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_sound">Max Sound File Size (KB)</span>
|
||
<input type="number" id="max-sound-kb" min="256" max="10240" value="1024" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_sound_hint">Max soundboard upload size (256–10240 KB, default 1024 / 1 MB)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_emoji">Max Emoji File Size (KB)</span>
|
||
<input type="number" id="max-emoji-kb" min="64" max="1024" value="256" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_emoji_hint">Max custom emoji upload size (64–1024 KB, default 256)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_sticker">Max Sticker File Size (KB)</span>
|
||
<input type="number" id="max-sticker-kb" min="256" max="10240" value="1024" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_sticker_hint">Max sticker upload size (256–10240 KB, default 1024 / 1 MB). Bump this up to allow larger GIF-style stickers.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_poll_options">Max Poll Options</span>
|
||
<input type="number" id="max-poll-options" min="2" max="25" value="10" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_poll_hint">Maximum answer choices per poll (2–25, default 10)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.session_duration">Login session duration</span>
|
||
<select id="session-duration-days" class="settings-select">
|
||
<option value="0" data-i18n="settings.admin.session_duration_never">Never expire (recommended)</option>
|
||
<option value="1" data-i18n="settings.admin.session_duration_1_day">1 day</option>
|
||
<option value="7" data-i18n="settings.admin.session_duration_7_days">7 days</option>
|
||
<option value="30" data-i18n="settings.admin.session_duration_30_days">30 days</option>
|
||
<option value="90" data-i18n="settings.admin.session_duration_90_days">90 days</option>
|
||
<option value="365" data-i18n="settings.admin.session_duration_365_days">365 days</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.session_duration_hint">How long a login token stays valid before users must sign in again. "Never expire" keeps users logged in until they hit Logout or change their password. Applies to new logins only; existing tokens keep their original expiry until they expire or rotate.</small>
|
||
</div>
|
||
<div class="admin-settings" id="section-connectivity" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">📡 <span data-i18n="settings.admin.connectivity_title">Voice & Connectivity</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.connectivity_hint">STUN/TURN servers let voice and screen share connect between users on different networks. Leave blank to use Haven's built-in STUN pool. If users outside your local network get stuck on "ICE: Connecting...", set your own servers here.</small>
|
||
<label class="select-row" style="margin-top:8px; align-items:flex-start">
|
||
<span data-i18n="settings.admin.stun_urls">STUN servers</span>
|
||
<textarea id="stun-urls-input" rows="3" class="settings-text-input" placeholder="stun:stun.cloudflare.com:3478 stun:global.stun.twilio.com:3478" style="flex:1; resize:vertical; font-family:monospace; font-size:0.75rem"></textarea>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.stun_urls_hint">One stun: URI per line (or comma separated). Blank = built-in defaults.</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.turn_url">TURN server (optional)</span>
|
||
<input type="text" id="turn-url-input" class="settings-text-input" placeholder="turn:turn.example.com:3478" style="flex:1">
|
||
</label>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.turn_username">TURN username</span>
|
||
<input type="text" id="turn-username-input" class="settings-text-input" autocomplete="off" style="flex:1">
|
||
</label>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.turn_password">TURN password</span>
|
||
<input type="password" id="turn-password-input" class="settings-text-input" autocomplete="new-password" style="flex:1">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.turn_hint">A TURN server relays traffic for users behind strict NATs that STUN alone can't connect. Only needed if some users still can't connect after setting STUN. Username/password apply to static-credential TURN servers.</small>
|
||
<div style="margin-top:12px; padding-top:10px; border-top:1px solid var(--border)">
|
||
<button class="btn-sm btn-accent" id="test-connectivity-btn">🩺 <span data-i18n="settings.admin.test_connectivity_btn">Test voice connectivity</span></button>
|
||
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.test_connectivity_hint">Checks the servers above the same way a call does, and says in plain English what works and what doesn't. Save your changes first.</small>
|
||
<div id="connectivity-test-result" style="display:none;margin-top:10px"></div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-sounds-admin" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🔊 <span data-i18n="settings.admin.sounds_title">Custom Sounds</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.sounds_hint">Upload audio files for custom notification sounds</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-accent" id="open-sound-manager-btn">⚙️ <span data-i18n="settings.admin.manage_sounds_btn">Manage Sounds</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-emojis" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">😎 <span data-i18n="settings.admin.emojis_title">Custom Emojis</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.emojis_hint">Upload images for custom server emojis</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-accent" id="open-emoji-manager-btn">⚙️ <span data-i18n="settings.admin.manage_emojis_btn">Manage Emojis</span></button>
|
||
</div>
|
||
<small class="settings-group-label" style="margin-top:14px;display:block">🔄 <span data-i18n="settings.admin.emoji_auto_update_group">Built-in emoji updates</span></small>
|
||
<small class="settings-hint" style="display:block" data-i18n="settings.admin.emoji_auto_update_desc">Allow Haven to check Unicode.org for new emoji standards. This check is only performed at server boot, and only when the current standard file on disk was created over a month ago. If a new standard is available, Haven will automatically download the emoji pack and regenerate all your built-in emojis based on the new standard. This does not affect your custom emojis in any way.</small>
|
||
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.emoji_auto_update_disk">When this setting is turned off, Haven will use the latest available standard on disk. If a standard was never downloaded before, Haven will default to Unicode standard 16.0 which is bundled with the server.</small>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.emoji_auto_update_label">Enable automatic Unicode.org upgrade</span>
|
||
<input type="checkbox" id="unicode-emoji-auto-update">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.emoji_auto_update_ps">PS: This feature has nothing to do with emoji styling. That is entirely dependent on your OS, and rendering will vary from machine to machine. This feature only guarantees you'll have the latest available emojis built into your Haven server.</small>
|
||
</div>
|
||
<div class="admin-settings" id="section-stickers" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🖼️ <span data-i18n="settings.admin.stickers_title">Stickers</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.stickers_hint">Upload sticker images grouped into packs</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-accent" id="open-sticker-manager-btn">⚙️ <span data-i18n="settings.admin.manage_stickers_btn">Manage Stickers</span></button>
|
||
</div>
|
||
</div>
|
||
<!-- Role Management (Admin only) -->
|
||
<div class="admin-settings" id="section-roles" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">👑 <span data-i18n="settings.admin.roles_title">Role Management</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.roles_hint">Create and manage roles. Assign roles to users from the user list context menu.</small>
|
||
<div style="margin-top:8px;display:flex;gap:6px">
|
||
<button class="btn-sm btn-accent" id="open-role-editor-btn">⚙️ <span data-i18n="settings.admin.manage_roles_btn">Manage Roles</span></button>
|
||
<button class="btn-sm" id="reset-roles-btn" data-i18n-title="settings.admin.reset_roles_title" title="Reset all roles to deployment defaults">🔄 <span data-i18n="settings.admin.reset_roles_btn">Reset to Default</span></button>
|
||
</div>
|
||
<div class="settings-group" style="margin-top:12px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.role_icon_visibility">Role Icon Visibility</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.role_icon_sidebar">Show role icons in sidebar</span>
|
||
<input type="checkbox" id="role-icon-sidebar" checked>
|
||
</label>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.role_icon_chat">Show role icons in chat</span>
|
||
<input type="checkbox" id="role-icon-chat">
|
||
</label>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.role_icon_after_name">Show role icons after username</span>
|
||
<input type="checkbox" id="role-icon-after-name">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.role_icon_hint">Controls where role icons appear. "After username" moves icons to the right side of the name instead of the left.</small>
|
||
</div>
|
||
<div class="settings-group" style="margin-top:12px">
|
||
<small class="settings-group-label" data-i18n="settings.admin.channel_creator_role_group">Channel Creator Role</small>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.channel_creator_role_label">Role given to a channel's creator</span>
|
||
<select id="channel-creator-role-select">
|
||
<option value="default" data-i18n="settings.admin.channel_creator_role_default">Default (highest channel role)</option>
|
||
<option value="none" data-i18n="settings.admin.channel_creator_role_none">None</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.channel_creator_role_hint">When a non-admin who can create channels makes one, they are given this role inside that channel so they can manage it. Admins are never affected.</small>
|
||
</div>
|
||
<div id="roles-list-preview" style="margin-top:8px;">
|
||
</div>
|
||
</div>
|
||
<!-- Tunnel settings (Admin only) -->
|
||
<div class="admin-settings" id="section-tunnel" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🧭 <span data-i18n="settings.admin.tunnel_title">Tunnel</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.tunnel_provider">Provider</span>
|
||
<select id="tunnel-provider-select">
|
||
<option value="localtunnel">LocalTunnel</option>
|
||
<option value="cloudflared">Cloudflared</option>
|
||
</select>
|
||
</label>
|
||
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
|
||
<button class="btn-sm btn-accent" id="tunnel-toggle-btn" data-i18n="settings.admin.tunnel_start_btn">Start Tunnel</button>
|
||
<span id="tunnel-status-display" class="muted-text" data-i18n="settings.admin.tunnel_inactive" style="font-size:0.6875rem;">Inactive</span>
|
||
</div>
|
||
</div>
|
||
<!-- Webhooks / Bots (Admin only) -->
|
||
<div class="admin-settings" id="section-bots" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🤖 <span data-i18n="settings.admin.bots_title">Webhooks / Bots</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.bots_hint">Create and manage webhook bots that can post messages to channels.</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-bot-editor-btn">⚙️ <span data-i18n="settings.admin.manage_bots_btn">Manage Bots</span></button>
|
||
</div>
|
||
<div id="webhooks-list" style="margin-top:8px;">
|
||
<p class="muted-text" data-i18n="settings.admin.no_bots">No bots configured</p>
|
||
</div>
|
||
</div>
|
||
<!-- Ferry: Haven <-> Discord bridge (Admin only) -->
|
||
<div class="admin-settings" id="section-ferry" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🛶 <span data-i18n="settings.admin.ferry_title">Ferry (Discord bridge)</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.ferry_hint">Relay messages between Haven channels and Discord channels. Needs your own Discord bot, which takes about two minutes to create.</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-ferry-btn">🛶 <span data-i18n="settings.admin.manage_ferry_btn">Set up Ferry</span></button>
|
||
</div>
|
||
<p class="muted-text" id="ferry-summary" style="margin-top:8px;font-size:.78rem;" data-i18n="settings.admin.ferry_not_configured">Not configured</p>
|
||
</div>
|
||
<!-- Custom Terms of Service (Admin only) -->
|
||
<div class="admin-settings" id="section-custom-tos" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">⚖️ <span data-i18n="settings.admin.custom_tos_title">Custom Terms of Service</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.custom_tos_hint">Add custom terms that appear before the default ToS on the login page. Supports plain text. Leave empty to show only the default Haven ToS.</small>
|
||
<textarea id="custom-tos-input" class="settings-textarea" rows="6" maxlength="50000" data-i18n-placeholder="settings.admin.custom_tos_placeholder" placeholder="Enter your custom terms of service..." style="margin-top:8px;width:100%;resize:vertical;min-height:80px;font-size:0.75rem;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-family:inherit;"></textarea>
|
||
</div>
|
||
<!-- Discord Import (Admin only) -->
|
||
<div class="admin-settings" id="section-import" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">📦 <span data-i18n="settings.admin.import_title">Discord Import</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.import_hint">Import channels and messages from a Discord export file</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-import-btn">📦 <span data-i18n="settings.admin.import_btn">Import from Discord</span></button>
|
||
</div>
|
||
</div>
|
||
<!-- Mod Mode (Admin only) -->
|
||
<div class="admin-settings" id="section-modmode" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🔧 <span data-i18n="settings.admin.modmode_title">Mod Mode</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.modmode_hint">Rearrange sidebar sections and snap server/sidebar/status panels</small>
|
||
<button class="btn-sm btn-full" id="mod-mode-settings-toggle" style="margin-top:6px;">🔧 <span data-i18n="settings.admin.modmode_toggle_btn">Toggle Mod Mode</span></button>
|
||
<button class="btn-sm btn-full" id="mod-mode-reset" data-i18n="settings.admin.modmode_reset_btn" style="margin-top:6px;">↺ Reset Layout</button>
|
||
</div>
|
||
<!-- Audit Log (Admin only) -->
|
||
<div class="admin-settings" id="section-audit-log" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">📜 <span data-i18n="settings.admin.audit_log_title">Audit Log</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.audit_log_hint">Track admin and moderator actions: channel changes, role updates, bans, kicks, server settings, and more.</small>
|
||
<button class="btn-sm btn-full btn-accent" id="open-audit-log-btn" style="margin-top:6px;">📜 <span data-i18n="settings.admin.audit_log_btn">View Audit Log</span></button>
|
||
</div>
|
||
</div>
|
||
</div><!-- /settings-body-admin -->
|
||
</div><!-- /settings-layout -->
|
||
<!-- Admin save bar — changes only apply when Save is clicked -->
|
||
<div class="admin-save-bar" style="display:none">
|
||
<button class="btn-accent btn-admin-save" id="admin-save-btn">💾 <span data-i18n="settings.admin.save_btn">Save Settings</span></button>
|
||
<small class="settings-hint" style="text-align:center;margin-top:4px" data-i18n="settings.admin.save_hint">Changes only apply when you click Save. Close (✕) to cancel.</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Discord Import Modal -->
|
||
<div class="modal-overlay" id="import-modal" style="display:none">
|
||
<div class="modal" style="width:560px">
|
||
<h3>📦 <span data-i18n="modals.discord_import.title">Import from Discord</span></h3>
|
||
|
||
<!-- Step 1: Upload or Connect -->
|
||
<div id="import-step-upload">
|
||
<div class="import-tabs">
|
||
<button class="import-tab active" data-tab="file">📁 <span data-i18n="modals.discord_import.file_tab">Upload File</span></button>
|
||
<button class="import-tab" data-tab="connect">🔗 <span data-i18n="modals.discord_import.connect_tab">Connect to Discord</span></button>
|
||
</div>
|
||
|
||
<!-- Tab: Upload File -->
|
||
<div id="import-tab-file">
|
||
<p class="settings-hint" style="margin-bottom:10px" data-i18n="modals.discord_import.upload_hint">
|
||
Upload a Discord export file to import channels and messages into Haven.
|
||
</p>
|
||
<div class="import-format-info">
|
||
<div class="import-format-row">
|
||
<strong>DiscordChatExporter</strong>
|
||
<span class="muted-text" data-i18n="modals.discord_import.format_dce">— JSON or ZIP (recommended — includes all users)</span>
|
||
</div>
|
||
<div class="import-format-row">
|
||
<strong>Discord Data Package</strong>
|
||
<span class="muted-text" data-i18n="modals.discord_import.format_ddp">— ZIP from Settings → Privacy (your messages only)</span>
|
||
</div>
|
||
</div>
|
||
<div class="import-dropzone" id="import-dropzone">
|
||
<input type="file" id="import-file-input" accept=".json,.zip" style="display:none">
|
||
<div class="import-dropzone-content">
|
||
<span class="import-dropzone-icon">📁</span>
|
||
<span><span data-i18n="modals.discord_import.drop_text">Drop file here or </span><a href="#" id="import-browse-link" data-i18n="modals.discord_import.browse_link">browse</a></span>
|
||
<span class="muted-text" style="font-size:0.6875rem" data-i18n="modals.discord_import.dropzone_hint">.json or .zip — up to 500 MB</span>
|
||
</div>
|
||
</div>
|
||
<div id="import-upload-progress" style="display:none">
|
||
<div class="import-progress-bar"><div class="import-progress-fill" id="import-progress-fill"></div></div>
|
||
<p class="muted-text" id="import-upload-status" style="margin-top:6px;text-align:center" data-i18n="modals.discord_import.uploading">Uploading...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Connect to Discord -->
|
||
<div id="import-tab-connect" style="display:none">
|
||
<div id="import-connect-step-token">
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="modals.discord_import.connect_hint">
|
||
Connect directly to Discord and pull your messages — no external tools needed.
|
||
</p>
|
||
<details class="import-token-help">
|
||
<summary data-i18n="modals.discord_import.token_help_summary">How do I get my Discord token?</summary>
|
||
<div class="import-token-steps">
|
||
<ol>
|
||
<li data-i18n-html="modals.discord_import.token_step_1">Open <a href="https://discord.com/app" target="_blank" rel="noopener">discord.com/app</a> in Chrome/Edge (or use the desktop app)</li>
|
||
<li data-i18n-html="modals.discord_import.token_step_2">Press <kbd>F12</kbd> to open DevTools</li>
|
||
<li data-i18n-html="modals.discord_import.token_step_3">Click the <strong>Application</strong> tab (click <code>>></code> in the toolbar if you don't see it)</li>
|
||
<li data-i18n-html="modals.discord_import.token_step_4">In the left sidebar, expand <strong>Local Storage</strong> → click <strong>https://discord.com</strong></li>
|
||
<li data-i18n-html="modals.discord_import.token_step_5">Find the <code>token</code> key — copy the value (without the quotes)</li>
|
||
</ol>
|
||
<p style="margin-top:8px" data-i18n-html="modals.discord_import.desktop_app_hint"><strong>Desktop app:</strong> If F12 doesn't open DevTools, close Discord fully, then edit:</p>
|
||
<code class="import-token-snippet">%appdata%\discord\settings.json</code>
|
||
<p style="margin-top:4px" data-i18n="modals.discord_import.add_line_hint">Add this line anywhere inside the { }:</p>
|
||
<code class="import-token-snippet">"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true</code>
|
||
<p style="margin-top:4px" data-i18n="modals.discord_import.restart_hint">Restart Discord, then follow the steps above.</p>
|
||
<p class="muted-text" style="margin-top:8px">⚠️ <span data-i18n="modals.discord_import.token_warning">Your token is like a password. Haven uses it only for this import and never stores it.</span></p>
|
||
</div>
|
||
</details>
|
||
<div style="display:flex;gap:8px;margin-top:10px">
|
||
<input type="password" id="import-discord-token" class="import-token-input" data-i18n-placeholder="modals.discord_import.token_placeholder" placeholder="Paste your Discord token here">
|
||
<button class="btn-sm btn-accent" id="import-connect-btn" data-i18n="modals.discord_import.connect_btn">Connect</button>
|
||
</div>
|
||
<p class="muted-text" id="import-connect-status" style="margin-top:6px;display:none"></p>
|
||
</div>
|
||
|
||
<div id="import-connect-step-servers" style="display:none">
|
||
<div class="import-info-bar" style="margin-bottom:8px">
|
||
<span><span data-i18n="modals.discord_import.connected_as">Connected as</span> <strong id="import-discord-username"></strong></span>
|
||
<a href="#" id="import-connect-disconnect" data-i18n="modals.discord_import.disconnect" style="font-size:0.6875rem;margin-left:auto">Disconnect</a>
|
||
</div>
|
||
<p class="settings-hint" style="margin-bottom:6px" data-i18n="modals.discord_import.pick_server">Pick a server to import from:</p>
|
||
<div id="import-server-list" class="import-server-grid"></div>
|
||
</div>
|
||
|
||
<div id="import-connect-step-channels" style="display:none">
|
||
<div class="import-info-bar" style="margin-bottom:8px">
|
||
<span id="import-connect-guild-name" style="font-weight:600"></span>
|
||
<a href="#" id="import-connect-back-servers" data-i18n="modals.discord_import.back_to_servers" style="font-size:0.6875rem;margin-left:auto">← Back to servers</a>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||
<p class="settings-hint" style="margin:0;flex:1" data-i18n="modals.discord_import.select_channels">Select channels to fetch:</p>
|
||
<a href="#" id="import-connect-toggle-all" data-i18n="modals.discord_import.deselect_all" style="font-size:0.6875rem;white-space:nowrap">Deselect All</a>
|
||
</div>
|
||
<div class="import-channel-list" id="import-connect-channel-list"></div>
|
||
<div style="display:flex;gap:8px;margin-top:10px;justify-content:flex-end">
|
||
<button class="btn-sm btn-accent" id="import-fetch-btn">📥 <span data-i18n="modals.discord_import.fetch_btn">Fetch Messages</span></button>
|
||
</div>
|
||
<p class="muted-text" id="import-fetch-status" style="margin-top:6px;display:none"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Preview & Select -->
|
||
<div id="import-step-preview" style="display:none">
|
||
<div class="import-info-bar">
|
||
<span class="import-format-badge" id="import-format-badge"></span>
|
||
<span id="import-server-name" style="font-weight:600"></span>
|
||
<span class="muted-text" id="import-total-msgs"></span>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin:8px 0 6px">
|
||
<p class="settings-hint" style="margin:0;flex:1" data-i18n="modals.discord_import.preview_select_hint">Select channels to import. You can rename them before importing.</p>
|
||
<a href="#" id="import-toggle-all" data-i18n="modals.discord_import.deselect_all" style="font-size:0.6875rem;white-space:nowrap">Deselect All</a>
|
||
</div>
|
||
<div class="import-channel-list" id="import-channel-list"></div>
|
||
<div style="display:flex;gap:8px;margin-top:12px;justify-content:flex-end">
|
||
<button class="btn-sm" id="import-back-btn" data-i18n="modals.common.back">← Back</button>
|
||
<button class="btn-sm btn-accent" id="import-execute-btn">📦 <span data-i18n="modals.discord_import.import_selected_btn">Import Selected</span></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Done -->
|
||
<div id="import-step-done" style="display:none;text-align:center;padding:20px 0">
|
||
<div style="font-size:3rem;margin-bottom:12px">✅</div>
|
||
<p id="import-done-msg" style="font-size:0.875rem" data-i18n="modals.discord_import.import_complete">Import complete!</p>
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top:12px">
|
||
<button class="btn-sm" id="close-import-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Role Management Modal -->
|
||
<div class="modal-overlay" id="role-modal" style="display:none">
|
||
<div class="modal modal-wide role-management-modal">
|
||
<h3>👑 <span data-i18n="modals.role_management.title">Role Management</span></h3>
|
||
<div class="role-editor-layout">
|
||
<div class="role-sidebar">
|
||
<div id="role-list-sidebar" class="role-list-sidebar"></div>
|
||
<button class="btn-sm btn-accent btn-full" id="create-role-btn" data-i18n="modals.role_management.new_role_btn" style="margin-top:8px;">+ New Role</button>
|
||
</div>
|
||
<div class="role-detail" id="role-detail-panel">
|
||
<p class="muted-text" style="padding:20px;text-align:center" data-i18n="modals.role_management.select_to_edit">Select a role to edit</p>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="save-role-btn" data-i18n="modals.common.save" style="display:none">Save</button>
|
||
<button class="btn-sm" id="close-role-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Role Members Modal (assign/remove role directly from role detail) -->
|
||
<div class="modal-overlay" id="role-members-modal" style="display:none">
|
||
<div class="modal" style="width:460px;max-height:80vh;display:flex;flex-direction:column;gap:0">
|
||
<h3 style="margin-bottom:4px">👥 <span id="role-members-modal-title"></span></h3>
|
||
<p class="muted-text" style="font-size:0.75rem;margin-bottom:10px" data-i18n="modals.role_management.members_scope_hint">Server-wide assignment only. For channel-specific roles, use the Role Assignment menu in member management.</p>
|
||
<input type="text" id="role-members-search" class="settings-text-input" data-i18n-placeholder="modals.role_management.search_members" placeholder="Search members…" style="margin-bottom:10px;flex-shrink:0">
|
||
<div id="role-members-list" style="overflow-y:auto;flex:1;border:1px solid var(--border);border-radius:6px;min-height:100px"></div>
|
||
<div class="modal-actions" style="margin-top:12px;flex-shrink:0">
|
||
<button class="btn-sm" id="role-members-close-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Assign Role Modal -->
|
||
<div class="modal-overlay" id="assign-role-modal" style="display:none">
|
||
<div class="modal" style="width:440px">
|
||
<h3>👑 <span data-i18n="modals.assign_role.title">Assign Role</span></h3>
|
||
<p id="assign-role-user-label" class="muted-text"></p>
|
||
|
||
<label class="assign-role-field-label" data-i18n="modals.assign_role.role_label">Roles</label>
|
||
<p class="assign-role-hint" data-i18n="modals.assign_role.role_hint_multi">A user may hold multiple roles. Their effective permissions are the union of every role they hold, and their displayed badge color comes from the highest role.</p>
|
||
<div id="assign-role-checkboxes" class="assign-role-checkboxes" role="group" data-i18n-aria-label="modals.assign_role.assignable_roles" aria-label="Assignable roles"></div>
|
||
|
||
<label class="assign-role-field-label" style="margin-top:10px" data-i18n="modals.assign_role.scope_label">Scope</label>
|
||
<p class="assign-role-hint" data-i18n="modals.assign_role.scope_hint">Server-wide applies to all channels. Channel-specific only applies within that channel.</p>
|
||
<select id="assign-role-scope" class="settings-select assign-role-select">
|
||
<option value="server" data-i18n="modals.assign_role.server_wide">Server-wide</option>
|
||
</select>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-assign-role-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="confirm-assign-role-btn" data-i18n="modals.assign_role.assign_btn">Assign</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Centralized Role Assignment (3-pane) Modal -->
|
||
<div class="modal-overlay" id="role-assign-center-modal" style="display:none">
|
||
<div class="modal role-assign-center-box">
|
||
<h3>👑 <span data-i18n="modals.role_assign_center.title">Role Assignment</span></h3>
|
||
<p class="muted-text role-assign-center-subtitle" data-i18n="modals.role_assign_center.subtitle">Select a user, then a channel, then configure their role.</p>
|
||
|
||
<div class="rac-layout">
|
||
<!-- Left pane: Users -->
|
||
<div class="rac-pane rac-users-pane">
|
||
<div class="rac-pane-header">
|
||
<span class="rac-pane-title" data-i18n="modals.role_assign_center.users_pane">Users</span>
|
||
<input type="text" id="rac-user-search" class="rac-search" data-i18n-placeholder="modals.role_assign_center.search_users_placeholder" placeholder="Search users…">
|
||
</div>
|
||
<div class="rac-pane-list" id="rac-user-list"></div>
|
||
</div>
|
||
|
||
<!-- Center pane: Channels -->
|
||
<div class="rac-pane rac-channels-pane">
|
||
<div class="rac-pane-header">
|
||
<span class="rac-pane-title" data-i18n="modals.role_assign_center.channels_pane">Channels</span>
|
||
</div>
|
||
<div class="rac-pane-list" id="rac-channel-list">
|
||
<p class="rac-placeholder" data-i18n="modals.role_assign_center.select_user_hint">← Select a user</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right pane: Role config -->
|
||
<div class="rac-pane rac-config-pane">
|
||
<div class="rac-pane-header">
|
||
<span class="rac-pane-title" data-i18n="modals.role_assign_center.config_pane">Role Configuration</span>
|
||
</div>
|
||
<div class="rac-config-body" id="rac-config-body">
|
||
<p class="rac-placeholder" data-i18n="modals.role_assign_center.select_channel_hint">← Select a channel</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions rac-footer">
|
||
<button class="btn-sm btn-accent" id="rac-manage-roles-btn" style="display:none" data-i18n-title="modals.role_assign_center.manage_roles_title" title="Open role management">⚙️ <span data-i18n="modals.role_assign_center.manage_roles_btn">Manage Roles</span></button>
|
||
<div class="rac-footer-right">
|
||
<button class="btn-sm" id="rac-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="rac-save-btn" data-i18n="modals.role_assign_center.save_btn" disabled>Save Changes</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bot Management Modal -->
|
||
<div class="modal-overlay" id="bot-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>🤖 <span data-i18n="modals.bot_mgmt.title">Bot Management</span></h3>
|
||
<div class="role-editor-layout">
|
||
<div class="role-sidebar">
|
||
<div id="bot-list-sidebar" class="role-list-sidebar"></div>
|
||
<button class="btn-sm btn-accent btn-full" id="create-bot-btn" data-i18n="modals.bot_mgmt.new_bot_btn" style="margin-top:8px;">+ New Bot</button>
|
||
</div>
|
||
<div class="role-detail" id="bot-detail-panel">
|
||
<p class="muted-text" style="padding:20px;text-align:center" data-i18n="modals.bot_mgmt.select_or_create">Select a bot to edit, or create a new one</p>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-bot-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sound Management Modal (Full Popout) -->
|
||
<div class="modal-overlay" id="sound-modal" style="display:none">
|
||
<div class="modal" style="width:640px;min-height:420px">
|
||
<h3>🔊 <span data-i18n="modals.sound_manager.title">Sound Manager</span></h3>
|
||
<!-- Tabs -->
|
||
<div class="sound-modal-tabs">
|
||
<button class="sound-tab active" data-tab="soundboard">🎵 <span data-i18n="modals.sound_manager.soundboard_tab">Soundboard</span></button>
|
||
<button class="sound-tab" data-tab="assign">🔔 <span data-i18n="modals.sound_manager.assign_tab">Assign to Events</span></button>
|
||
<button class="sound-tab sound-tab-admin" data-tab="manage" style="display:none">⚙️ <span data-i18n="modals.sound_manager.manage_tab">Manage</span></button>
|
||
</div>
|
||
|
||
<!-- TAB: Soundboard (all users) -->
|
||
<div class="sound-tab-content active" id="sound-tab-soundboard">
|
||
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n="modals.sound_manager.modal_hint">Browse & play custom sounds. Set hotkeys to play them anytime.</small>
|
||
<div class="sound-search-row">
|
||
<input type="text" id="soundboard-search" data-i18n-placeholder="modals.sound_manager.search_placeholder" placeholder="Search sounds..." class="settings-text-input" style="flex:1">
|
||
<button id="soundboard-popout-btn" class="icon-btn small" data-i18n-title="media_runtime.sound.popout" title="Pop out soundboard">⧉</button>
|
||
<div style="display:flex;gap:8px;margin-bottom:8px;font-size:0.75rem">
|
||
<label style="display:flex;align-items:center;gap:4px"><input type="checkbox" id="soundboard-show-hidden" style="cursor:pointer"> <span data-i18n="modals.sound_manager.show_hidden">Show hidden</span></label>
|
||
<label style="display:flex;align-items:center;gap:4px"><input type="checkbox" id="soundboard-list-mode" style="cursor:pointer"> <span data-i18n="modals.sound_manager.list_view">List view</span></label>
|
||
<label style="display:flex;align-items:center;gap:4px"><input type="checkbox" id="soundboard-sidebar-mode" style="cursor:pointer"> <span data-i18n="settings.soundboard_section.sidebar_mode">Sidebar mode</span></label>
|
||
</div>
|
||
</div>
|
||
<div id="soundboard-grid" class="soundboard-grid">
|
||
<p class="muted-text" data-i18n="modals.sound_manager.no_sounds">No sounds available</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Assign to Events (all users) -->
|
||
<div class="sound-tab-content" id="sound-tab-assign">
|
||
<small class="settings-hint" style="display:block;margin-bottom:10px" data-i18n="modals.sound_manager.assign_hint">Choose which sound plays for each event. Custom sounds uploaded by the admin appear here too.</small>
|
||
<div class="sound-assign-list">
|
||
<label class="select-row">
|
||
<span>📨 <span data-i18n="modals.sound_manager.new_message">New Message</span></span>
|
||
<select id="assign-msg-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>📤 <span data-i18n="modals.sound_manager.msg_sent">Message Sent</span></span>
|
||
<select id="assign-sent-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>📢 <span data-i18n="modals.sound_manager.mention">@Mention</span></span>
|
||
<select id="assign-mention-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>👋 <span data-i18n="modals.sound_manager.user_joined">User Joined</span></span>
|
||
<select id="assign-join-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>🚪 <span data-i18n="modals.sound_manager.user_left">User Left</span></span>
|
||
<select id="assign-leave-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Manage (admin only) -->
|
||
<div class="sound-tab-content" id="sound-tab-manage">
|
||
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="modals.sound_manager.upload_hint">Upload audio files for custom sounds (max 1 MB each, mp3/ogg/wav/webm)</small>
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="sound-name-input" data-i18n-placeholder="modals.sound_manager.name_placeholder" placeholder="Sound name..." maxlength="30" class="settings-text-input">
|
||
<input type="file" id="sound-file-input" accept="audio/*" style="max-width:120px;font-size:0.6875rem">
|
||
<button class="btn-sm btn-accent" id="sound-upload-btn" data-i18n="modals.sound_manager.upload_btn">Upload</button>
|
||
</div>
|
||
<div id="custom-sounds-list" style="margin-top:12px;">
|
||
<p class="muted-text" data-i18n="modals.sound_manager.no_custom_sounds">No custom sounds uploaded</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-sound-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- (soundboard sidebar panel is now in-flow before #right-sidebar) -->
|
||
|
||
<!-- Emoji Management Modal -->
|
||
<div class="modal-overlay" id="emoji-modal" style="display:none">
|
||
<div class="modal" style="width:520px">
|
||
<h3>😎 <span data-i18n="modals.emoji_mgmt.title">Emoji Management</span></h3>
|
||
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="settings.admin.emojis_upload_hint">Upload images for custom server emojis (max 256 KB, png/gif/webp). Name must be lowercase, no spaces.</small>
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="emoji-name-input" data-i18n-placeholder="modals.emoji_mgmt.name_placeholder" placeholder="emoji_name" maxlength="30" class="settings-text-input">
|
||
<input type="file" id="emoji-file-input" accept="image/png,image/gif,image/webp,image/jpeg" style="max-width:120px;font-size:0.6875rem">
|
||
<button class="btn-sm btn-accent" id="emoji-upload-btn" data-i18n="modals.emoji_mgmt.upload_btn">Upload</button>
|
||
</div>
|
||
<div style="margin-top:6px;text-align:center">
|
||
<label class="btn-sm" style="cursor:pointer;display:inline-block" data-i18n-title="modals.emoji_mgmt.bulk_title" title="Select multiple images — names auto-generated from filenames">
|
||
📦 <span data-i18n="modals.emoji_mgmt.bulk_upload">Bulk Upload</span>
|
||
<input type="file" id="emoji-bulk-input" accept="image/png,image/gif,image/webp,image/jpeg" multiple style="display:none">
|
||
</label>
|
||
</div>
|
||
<div id="emoji-crop-preview-row" style="display:none;align-items:center;gap:10px;margin-top:8px;padding:8px;background:var(--bg-tertiary);border-radius:var(--radius-sm)">
|
||
<img id="emoji-crop-thumb" src="" alt="" style="width:32px;height:32px;border-radius:4px;object-fit:cover;border:1px solid var(--border)">
|
||
<span class="settings-hint" style="flex:1" data-i18n="modals.emoji_mgmt.crop_applied">Crop applied — ready to upload</span>
|
||
<button class="btn-xs" id="emoji-recrop-btn" data-i18n-title="modals.emoji_mgmt.recrop_title" title="Re-open cropper">✏️ <span data-i18n="modals.emoji_mgmt.adjust_btn">Adjust</span></button>
|
||
</div>
|
||
<div id="custom-emojis-list" style="margin-top:12px;">
|
||
<p class="muted-text" data-i18n="modals.emoji_mgmt.no_emojis">No custom emojis uploaded</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-emoji-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sticker Management Modal -->
|
||
<div class="modal-overlay" id="sticker-modal" style="display:none">
|
||
<div class="modal" style="width:520px">
|
||
<h3>🖼️ <span data-i18n="modals.sticker_mgmt.title">Sticker Management</span></h3>
|
||
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="modals.sticker_mgmt.upload_hint">Upload sticker images (max 1 MB, png/gif/webp). Group stickers into packs by setting the same pack name. Sticker name must be lowercase, no spaces.</small>
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="sticker-name-input" data-i18n-placeholder="modals.sticker_mgmt.name_placeholder" placeholder="sticker_name (optional)" maxlength="40" class="settings-text-input">
|
||
<input type="text" id="sticker-pack-input" data-i18n-placeholder="modals.sticker_mgmt.pack_placeholder" placeholder="Pack name (optional)" maxlength="40" class="settings-text-input">
|
||
<input type="file" id="sticker-file-input" accept="image/png,image/gif,image/webp,image/jpeg" style="max-width:120px;font-size:0.6875rem">
|
||
<button class="btn-sm btn-accent" id="sticker-upload-btn" data-i18n="modals.sticker_mgmt.upload_btn">Upload</button>
|
||
</div>
|
||
<div style="margin-top:6px;text-align:center">
|
||
<label class="btn-sm" style="cursor:pointer;display:inline-block" data-i18n-title="modals.sticker_mgmt.bulk_title" title="Select multiple images - names auto-generated from filenames; optional pack applies to all">
|
||
📦 <span data-i18n="modals.emoji_mgmt.bulk_upload">Bulk Upload</span>
|
||
<input type="file" id="sticker-bulk-input" accept="image/png,image/gif,image/webp,image/jpeg" multiple style="display:none">
|
||
</label>
|
||
</div>
|
||
<div id="stickers-list" style="margin-top:12px;">
|
||
<p class="muted-text" data-i18n="modals.sticker_mgmt.no_stickers">No stickers uploaded</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-sticker-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Emoji Crop Modal -->
|
||
<div class="modal-overlay" id="emoji-crop-modal" style="display:none">
|
||
<div class="modal" style="width:320px;text-align:center">
|
||
<h3 style="margin-bottom:6px">✂️ <span data-i18n="modals.emoji_crop.title">Crop Emoji</span></h3>
|
||
<small class="settings-hint" style="display:block;margin-bottom:10px" data-i18n="modals.emoji_crop.hint">Drag to reposition · Slider to zoom</small>
|
||
<canvas id="emoji-crop-canvas" width="256" height="256" style="border-radius:var(--radius);cursor:grab;display:block;margin:0 auto 10px;border:2px solid var(--accent);max-width:100%;touch-action:none"></canvas>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;padding:0 4px">
|
||
<span style="font-size:0.8125rem" data-i18n-title="modals.emoji_crop.zoom_out" title="Zoom out">🔍</span>
|
||
<input type="range" id="emoji-crop-zoom" min="100" max="500" value="100" step="1" style="flex:1;accent-color:var(--accent)">
|
||
<span style="font-size:0.8125rem" data-i18n-title="modals.emoji_crop.zoom_in" title="Zoom in">🔎</span>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="emoji-crop-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="emoji-crop-confirm-btn" data-i18n="modals.emoji_crop.confirm_btn">Use This Crop</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ferry (Discord bridge) Modal -->
|
||
<div class="modal-overlay" id="ferry-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>🛶 <span data-i18n="modals.ferry.title">Ferry (Discord bridge)</span></h3>
|
||
<div id="ferry-modal-body" class="ferry-body"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="ferry-refresh-btn" data-i18n="modals.ferry.refresh">Reconnect</button>
|
||
<button class="btn-sm btn-accent" id="ferry-close-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audit Log Modal -->
|
||
<div class="modal-overlay" id="audit-log-modal" style="display:none">
|
||
<div class="modal modal-wide" style="width:760px">
|
||
<h3>📜 <span data-i18n="modals.audit_log.title">Audit Log</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.audit_log.desc">A record of admin and moderator actions on this server.</p>
|
||
<div class="modal-flex-body">
|
||
<div class="audit-log-toolbar">
|
||
<label class="organize-toolbar-label" data-i18n="modals.audit_log.filter_action">Action:</label>
|
||
<select id="audit-log-filter-action">
|
||
<option value="" data-i18n="modals.audit_log.all_actions">All actions</option>
|
||
</select>
|
||
<label class="organize-toolbar-label" data-i18n="modals.audit_log.filter_actor">Actor:</label>
|
||
<input type="text" id="audit-log-filter-actor" data-i18n-placeholder="settings.admin.whitelist_username_placeholder" placeholder="Username..." maxlength="40" style="width:140px">
|
||
<button class="btn-sm btn-accent" id="audit-log-refresh-btn">🔄 <span data-i18n="modals.audit_log.refresh">Refresh</span></button>
|
||
<div class="organize-spacer"></div>
|
||
<button class="btn-sm btn-accent" id="audit-log-export-btn">📥 <span data-i18n="modals.audit_log.export">Export JSON</span></button>
|
||
</div>
|
||
<div id="audit-log-list" class="audit-log-list">
|
||
<div class="audit-log-empty" data-i18n="modals.audit_log.loading">Loading...</div>
|
||
</div>
|
||
<button class="btn-sm audit-log-load-more" id="audit-log-load-more" style="display:none" data-i18n="modals.audit_log.load_more">Load 50 More</button>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-audit-log-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="bans-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3 data-i18n="modals.bans.title">Banned Users</h3>
|
||
<div class="modal-flex-body">
|
||
<div id="bans-list" class="bans-list modal-grow" style="overflow:auto">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-bans-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="ip-bans-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>🚫 <span data-i18n="modals.ip_bans.title">Banned IPs</span></h3>
|
||
<div class="modal-flex-body">
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">
|
||
<input type="text" id="ip-ban-input" data-i18n-placeholder="modals.ip_bans.ip_placeholder" placeholder="IP address (e.g. 203.0.113.42)" class="settings-text-input" style="flex:1;min-width:160px" maxlength="64">
|
||
<input type="text" id="ip-ban-reason-input" data-i18n-placeholder="modals.ip_bans.reason_placeholder" placeholder="Reason (optional)" class="settings-text-input" style="flex:1;min-width:160px" maxlength="200">
|
||
<button class="btn-sm btn-accent btn-danger-fill" id="ip-ban-add-btn" data-i18n="modals.ip_bans.add_btn">Ban IP</button>
|
||
</div>
|
||
<div id="ip-bans-list" class="bans-list modal-grow" style="overflow:auto">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-ip-bans-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="deleted-users-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>🗑️ <span data-i18n="modals.deleted_users.title">Deleted Users</span></h3>
|
||
<div class="modal-flex-body">
|
||
<div id="deleted-users-list" class="bans-list modal-grow" style="overflow:auto">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-deleted-users-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- All Members Modal -->
|
||
<div class="modal-overlay" id="all-members-modal" style="display:none">
|
||
<div class="modal modal-wide" style="max-width:720px;max-height:85vh;display:flex;flex-direction:column">
|
||
<h3>👥 <span data-i18n="modals.all_members.title">All Members</span> <span id="all-members-count" class="muted-text" style="font-size:0.8125rem;font-weight:normal"></span></h3>
|
||
<div style="margin-bottom:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<input type="text" id="all-members-search" data-i18n-placeholder="modals.all_members.search_placeholder" placeholder="Search users..." maxlength="40" class="settings-text-input" style="flex:1;min-width:140px">
|
||
<select id="all-members-filter" style="font-size:0.75rem;padding:4px 8px;border-radius:var(--radius-sm);background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border)">
|
||
<option value="all" data-i18n="modals.all_members.filter_all">All</option>
|
||
<option value="online" data-i18n="modals.all_members.filter_online">Online</option>
|
||
<option value="offline" data-i18n="modals.all_members.filter_offline">Offline</option>
|
||
<option value="new" data-i18n="modals.all_members.filter_new">New (7 days)</option>
|
||
<option value="banned" data-i18n="modals.all_members.filter_banned">Banned</option>
|
||
<option value="storage" data-i18n="modals.all_members.filter_storage">Most storage used</option>
|
||
</select>
|
||
</div>
|
||
<div id="all-members-storage-summary" class="settings-hint" style="display:none;margin:-4px 0 8px"></div>
|
||
<div id="all-members-list" style="overflow-y:auto;flex:1;min-height:0;display:flex;flex-direction:column;gap:4px">
|
||
<p class="muted-text" style="text-align:center;padding:20px" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions" style="justify-content:space-between;flex-wrap:wrap;gap:6px">
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
<button class="btn-sm btn-accent" id="aml-view-invite-btn" style="display:none">➕ <span data-i18n="settings.admin.view_invite_btn">Invite Users</span></button>
|
||
<button class="btn-sm btn-accent" id="aml-view-bans-btn" style="display:none">📋 <span data-i18n="settings.admin.view_bans_btn">View Bans</span></button>
|
||
<button class="btn-sm btn-accent" id="aml-view-deleted-btn" style="display:none">🗑️ <span data-i18n="settings.admin.view_deleted_btn">View Deleted Users</span></button>
|
||
<button class="btn-sm btn-danger-fill" id="aml-bulk-cleanup-btn" style="display:none">🧹 <span data-i18n="settings.admin.bulk_cleanup_btn">Cleanup</span></button>
|
||
</div>
|
||
<button class="btn-sm" id="close-all-members-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Invite Links Modal -->
|
||
<div class="modal-overlay" id="invite-links-modal" style="display:none">
|
||
<div class="modal modal-wide" style="max-width:640px;max-height:85vh;display:flex;flex-direction:column">
|
||
<h3 data-i18n="settings.admin.invite_links_title">🔗 Invite Links</h3>
|
||
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n-html="settings.admin.invite_links_modal_hint">Shareable links that drop people straight into the channels you pick. Share <code>https://your-server/?invite=CODE</code> or <code>/invite/CODE</code>. Private channels are never granted by a link.</small>
|
||
<div id="invite-codes-list" style="overflow-y:auto;flex:1;min-height:60px;display:flex;flex-direction:column;gap:8px;padding-right:2px">
|
||
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.invite_links_loading">Loading invite links…</p>
|
||
</div>
|
||
<details id="invite-code-create" style="margin-top:12px;border:1px solid var(--border);border-radius:8px;padding:10px;flex:none">
|
||
<summary style="cursor:pointer;font-weight:600;font-size:0.9rem" data-i18n="settings.admin.invite_links_create_title">➕ Create a new invite link</summary>
|
||
<div style="margin-top:10px;display:flex;flex-direction:column;gap:10px">
|
||
<label class="select-row" style="align-items:center">
|
||
<span><span data-i18n="settings.admin.invite_links_label">Label</span> <small class="settings-hint" style="display:block" data-i18n="settings.admin.invite_links_label_hint">For your reference, e.g. "Reddit promo"</small></span>
|
||
<input type="text" id="invite-new-label" maxlength="60" class="settings-text-input" style="max-width:200px" data-i18n-placeholder="settings.admin.invite_links_label_placeholder" placeholder="Optional name">
|
||
</label>
|
||
<div>
|
||
<h6 style="margin:0 0 4px;font-size:0.82rem;font-weight:600" data-i18n="settings.admin.invite_links.channels_granted">Channels this link grants</h6>
|
||
<div id="invite-new-channels" style="margin-top:6px;max-height:180px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px">
|
||
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.loading_channels">Loading channels…</p>
|
||
</div>
|
||
<div style="display:flex;gap:4px;margin-top:4px">
|
||
<button class="btn-sm" id="invite-new-channels-all" type="button" data-i18n="settings.admin.invite_links.select_all">Select all</button>
|
||
<button class="btn-sm" id="invite-new-channels-none" type="button" data-i18n="settings.admin.invite_links.select_none">Select none</button>
|
||
</div>
|
||
</div>
|
||
<label class="select-row" style="align-items:center">
|
||
<span><span data-i18n="settings.admin.invite_links_max_uses">Max uses</span> <small class="settings-hint" style="display:block" data-i18n="settings.admin.invite_links_max_uses_hint">0 = unlimited. Counts distinct people.</small></span>
|
||
<input type="number" id="invite-new-maxuses" min="0" max="100000" value="1" class="settings-number-input">
|
||
</label>
|
||
<label class="select-row" style="align-items:center">
|
||
<span data-i18n="settings.admin.invite_links.expires">Expires</span>
|
||
<select id="invite-new-expiry" class="settings-number-input">
|
||
<option value="0" data-i18n="settings.admin.invite_links.never">Never</option>
|
||
<option value="1" data-i18n="settings.admin.invite_links.after_hour">After 1 hour</option>
|
||
<option value="24" data-i18n="settings.admin.invite_links.after_day">After 1 day</option>
|
||
<option value="168" data-i18n="settings.admin.invite_links.after_7_days">After 7 days</option>
|
||
<option value="720" selected data-i18n="settings.admin.invite_links.after_30_days">After 30 days</option>
|
||
</select>
|
||
</label>
|
||
<label class="select-row" style="align-items:center">
|
||
<span><span data-i18n="settings.admin.invite_links_custom_code">Custom code</span> <small class="settings-hint" style="display:block" data-i18n="settings.admin.invite_links_custom_code_hint">Optional. 3-32 chars. Blank = random.</small></span>
|
||
<input type="text" id="invite-new-slug" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="e.g. summer2026">
|
||
</label>
|
||
<div>
|
||
<button class="btn-sm btn-accent" id="invite-create-btn" type="button" data-i18n="settings.admin.invite_links_create">Create invite link</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<div class="modal-actions" style="justify-content:flex-end">
|
||
<button class="btn-sm" id="close-invite-links-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Music Share Modal -->
|
||
<div class="modal-overlay" id="music-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3>🎵 <span data-i18n="modals.listen_together.title">Listen Together</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.listen_together.desc">Paste a link from Spotify, YouTube, or SoundCloud to share with everyone in voice</p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.listen_together.link_label">Music Link</label>
|
||
<input type="text" id="music-link-input" data-i18n-placeholder="modals.listen_together.link_placeholder" placeholder="https://open.spotify.com/track/..." maxlength="500">
|
||
</div>
|
||
<div class="music-link-preview" id="music-link-preview"></div>
|
||
<div class="music-platforms-hint">
|
||
<span class="platform-badge spotify">Spotify</span>
|
||
<span class="platform-badge youtube">YouTube</span>
|
||
<span class="platform-badge soundcloud">SoundCloud</span>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-music-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<div style="display:flex;flex-direction:column;gap:6px;align-items:stretch">
|
||
<button class="btn-sm btn-accent" id="share-music-btn" data-i18n="modals.listen_together.share_btn">Share with Channel</button>
|
||
<button class="btn-sm btn-accent" id="share-music-playlist-btn" style="display:none" data-i18n="media.share_playlist">Share Playlist with Channel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="music-queue-modal" style="display:none">
|
||
<div class="modal modal-wide music-queue-modal">
|
||
<h3>📋 <span data-i18n="media.music_queue_title">Playback Queue</span></h3>
|
||
<p class="modal-desc" id="music-queue-summary" data-i18n="media.music_queue_empty_summary">No queued tracks</p>
|
||
<div class="music-queue-table-wrap">
|
||
<table class="music-queue-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="music-queue-col-handle"></th>
|
||
<th class="music-queue-col-pos" data-i18n="media.music_queue_col_pos">Pos.</th>
|
||
<th class="music-queue-col-requested-by" data-i18n="media.music_queue_col_requested_by">Requested By</th>
|
||
<th class="music-queue-col-title" data-i18n="media.music_queue_col_title">Title</th>
|
||
<th class="music-queue-col-actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="music-queue-body">
|
||
<tr><td colspan="5" class="music-queue-empty" data-i18n="media.music_queue_is_empty">Queue is empty</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="shuffle-music-queue-btn" style="display:none" data-i18n="media.music_shuffle_queue">Shuffle Queue</button>
|
||
<button class="btn-sm" id="close-music-queue-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Manage Servers Modal -->
|
||
<div class="modal-overlay" id="manage-servers-modal" style="display:none">
|
||
<div class="modal modal-flex manage-servers-modal-inner">
|
||
<h3>⚙ <span data-i18n="modals.manage_servers.title">Manage Servers</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.manage_servers.desc">Edit or remove your linked servers</p>
|
||
<div id="manage-servers-list" class="manage-servers-list"></div>
|
||
<div class="modal-actions" style="justify-content:space-between">
|
||
<button class="btn-sm btn-accent" id="manage-servers-add-btn" data-i18n="modals.manage_servers.add_btn">+ Add Server</button>
|
||
<button class="btn-sm" id="manage-servers-close-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel context menu (appears on "..." hover button) -->
|
||
<div id="channel-ctx-menu" class="channel-ctx-menu" style="display:none">
|
||
<button class="channel-ctx-item" data-action="mark-read">✅ <span data-i18n="context_menu.channel.mark_read">Mark as Read</span></button>
|
||
<button class="channel-ctx-item" data-action="mute">🔔 <span data-i18n="context_menu.channel.mute">Mute Channel</span></button>
|
||
<button class="channel-ctx-item" data-action="copy-channel-link">🔗 <span data-i18n="context_menu.channel.copy_link">Copy Channel Link</span></button>
|
||
<button class="channel-ctx-item" data-action="join-voice">🎙️ <span data-i18n="voice.join_ctx">Join Voice</span></button>
|
||
<button class="channel-ctx-item" data-action="leave-voice" style="display:none">📴 <span data-i18n="context_menu.channel.disconnect_voice">Disconnect Voice</span></button>
|
||
<hr class="channel-ctx-sep">
|
||
<button class="channel-ctx-item" data-action="leave-channel">🚪 <span data-i18n="context_menu.channel.leave">Leave Channel</span></button>
|
||
<button class="channel-ctx-item" data-action="hide-channel" style="display:none">🙈 <span data-i18n="context_menu.channel.hide">Hide Channel</span></button>
|
||
<hr class="channel-ctx-sep rename-or-createSub">
|
||
<button class="channel-ctx-item mod-only" data-action="rename-channel">✏️ <span data-i18n="context_menu.channel.rename">Rename Channel</span></button>
|
||
<button class="channel-ctx-item mod-only" data-action="create-sub-channel">📁 <span data-i18n="context_menu.channel.create_sub">Create Sub-channel</span></button>
|
||
<hr class="channel-ctx-sep admin-only">
|
||
<button class="channel-ctx-item admin-only" data-action="organize" style="display:none">📋 <span data-i18n="context_menu.channel.organize">Organize</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="move-to-parent" style="display:none">📦 <span data-i18n="context_menu.channel.move_to">Move to…</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="promote-channel" style="display:none">⬆️ <span data-i18n="context_menu.channel.promote">Promote to Channel</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="channel-functions">⚙️ <span data-i18n="context_menu.channel.functions">Channel Functions</span> <span class="cfn-arrow">▶</span></button>
|
||
<div id="channel-functions-panel" class="channel-functions-panel" style="display:none">
|
||
<div class="cfn-row" data-fn="voice" data-i18n-title="channel_functions.voice_hint" title="Enable or disable voice chat in this channel">
|
||
<span class="cfn-label">🎙️ <span data-i18n="channel_functions.voice">Voice</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="text" data-i18n-title="channel_functions.text_hint" title="Enable or disable text chat in this channel">
|
||
<span class="cfn-label">💬 <span data-i18n="channel_functions.text">Text</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="streams" data-i18n-title="channel_functions.streams_hint" title="Allow screen sharing and camera (requires voice)">
|
||
<span class="cfn-label">🖥️ <span data-i18n="channel_functions.streams">Streams</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="music" data-i18n-title="channel_functions.music_hint" title="Allow Listen Together / music playback (requires voice)">
|
||
<span class="cfn-label">🎵 <span data-i18n="channel_functions.music">Music</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="soundboard" data-i18n-title="channel_functions.soundboard_hint" title="Allow the soundboard in this channel's voice chat">
|
||
<span class="cfn-label">🔊 <span data-i18n="channel_functions.soundboard">Soundboard</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="media" data-i18n-title="channel_functions.media_hint" title="Allow image, video, and file uploads in this channel">
|
||
<span class="cfn-label">🖼️ <span data-i18n="channel_functions.media">Media</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="read-only" data-i18n-title="channel_functions.read_only_hint" title="Make this channel read-only — only users with the Read-Only Override permission can post">
|
||
<span class="cfn-label">🔒 <span data-i18n="channel_functions.read_only">Read Only</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="forum" data-i18n-title="channel_functions.forum_hint" title="Each message is a topic. Replies go in its thread and bump the topic back to the newest end of the channel">
|
||
<span class="cfn-label">🗂️ <span data-i18n="channel_functions.forum">Forum</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-divider"></div>
|
||
<div class="cfn-row" data-fn="slow-mode" data-i18n-title="channel_functions.slow_mode_hint" title="Require a cooldown between messages (0 = off)">
|
||
<span class="cfn-label">🐢 <span data-i18n="channel_functions.slow_mode">Slow Mode</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="cleanup-exempt" data-i18n-title="channel_functions.cleanup_guard_hint" title="Protect this channel from automatic message cleanup">
|
||
<span class="cfn-label">🛡️ <span data-i18n="channel_functions.cleanup_guard">Cleanup Guard</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="welcome" data-i18n-title="channel_functions.welcome_hint" title="Post a saved welcome message here when a new member joins the server">
|
||
<span class="cfn-label">👋 <span data-i18n="channel_functions.welcome">Welcome Messages</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="user-limit" data-i18n-title="channel_functions.voice_limit_hint" title="Limit simultaneous voice users (click to set; blank = unlimited)">
|
||
<span class="cfn-label">👥 <span data-i18n="channel_functions.voice_limit">Voice Limit</span></span>
|
||
<span class="cfn-badge cfn-off">∞</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="voice-bitrate" data-i18n-title="channel_functions.voice_bitrate_hint" title="Cap voice audio bitrate in kbps (0 = auto / no cap)">
|
||
<span class="cfn-label">🎚️ <span data-i18n="channel_functions.voice_bitrate">Voice Bitrate</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.voice_bitrate_auto">Auto</span>
|
||
</div>
|
||
<div class="cfn-divider"></div>
|
||
<div class="cfn-row" data-fn="self-destruct" data-i18n-title="channel_functions.self_destruct_hint" title="Set a self-destruct timer — channel auto-deletes after the specified hours (0 = off)">
|
||
<span class="cfn-label">⏱️ <span data-i18n="channel_functions.self_destruct">Self Destruct</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="announcement" data-i18n-title="channel_functions.announcement_hint" title="Announcement channel — messages use a distinct notification sound and gold highlight">
|
||
<span class="cfn-label">📢 <span data-i18n="channel_functions.announcement">Announcement</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="default-role" data-i18n-title="channel_functions.default_role_hint" title="Auto-grant this role (scoped to this channel) to every current and future member">
|
||
<span class="cfn-label">👑 <span data-i18n="channel_functions.default_role">Default Role</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.none">None</span>
|
||
</div>
|
||
<div class="cfn-divider cfn-afk-divider" style="display:none"></div>
|
||
<div class="cfn-row cfn-afk-row" data-fn="afk-sub" style="display:none" data-i18n-title="channel_functions.afk_sub_hint" title="Designate a sub-channel as the AFK voice room — idle users are moved here">
|
||
<span class="cfn-label">💤 <span data-i18n="channel_functions.afk_sub">AFK Sub</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row cfn-afk-row" data-fn="afk-timeout" style="display:none" data-i18n-title="channel_functions.afk_timeout_hint" title="Minutes of voice inactivity before moving to AFK sub (0 = disabled)">
|
||
<span class="cfn-label">⏱️ <span data-i18n="channel_functions.afk_timeout">AFK Timeout</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
</div>
|
||
<button class="channel-ctx-item admin-only" data-action="channel-roles">👑 <span data-i18n="context_menu.channel.roles">Roles</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="webhooks">🤖 <span data-i18n="context_menu.channel.webhooks">Webhooks</span></button>
|
||
<hr class="channel-ctx-sep admin-only">
|
||
<button class="channel-ctx-item admin-only danger" data-action="delete">🗑️ <span data-i18n="context_menu.channel.delete">Delete Channel</span></button>
|
||
</div>
|
||
|
||
<!-- DM context menu -->
|
||
<div id="dm-ctx-menu" class="channel-ctx-menu" style="display:none">
|
||
<button class="channel-ctx-item" data-action="dm-mark-read">✅ <span data-i18n="context_menu.dm.mark_read">Mark as Read</span></button>
|
||
<button class="channel-ctx-item" data-action="dm-mute">🔔 <span data-i18n="context_menu.dm.mute">Mute DM</span></button>
|
||
<hr class="channel-ctx-sep">
|
||
<button class="channel-ctx-item danger" data-action="dm-delete">🗑️ <span data-i18n="context_menu.dm.delete">Delete DM</span></button>
|
||
</div>
|
||
|
||
<!-- Organize Sub-channels Modal -->
|
||
<div class="modal-overlay" id="organize-modal" style="display:none">
|
||
<div class="modal" style="width:500px">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||
<button id="organize-back-btn" class="btn-sm" style="display:none;padding:3px 8px;font-size:0.8rem" data-i18n-title="modals.organize.back_title" title="Back to channel list"><span data-i18n="modals.organize.back_btn">← Back</span></button>
|
||
<h3 id="organize-modal-title" style="margin:0">📋 <span data-i18n="modals.organize.title">Organize</span></h3>
|
||
</div>
|
||
<p class="modal-desc" id="organize-modal-parent-name"></p>
|
||
|
||
<div class="modal-flex-body">
|
||
<div class="organize-toolbar">
|
||
<label class="organize-toolbar-label" data-i18n="modals.organize.sort_label">Sort:</label>
|
||
<select id="organize-global-sort" class="organize-select">
|
||
<option value="server_default" data-i18n="modals.organize.server_default">Server Default</option>
|
||
<option value="manual" data-i18n="modals.organize.sort_manual">Manual</option>
|
||
<option value="alpha" data-i18n="modals.organize.sort_alpha">Alphabetical</option>
|
||
<option value="created" data-i18n="modals.organize.sort_newest">Newest First</option>
|
||
<option value="oldest" data-i18n="modals.organize.sort_oldest">Oldest First</option>
|
||
<option value="dynamic" data-i18n="modals.organize.sort_dynamic">Dynamic</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="organize-toolbar" id="organize-cat-toolbar" style="display:none">
|
||
<label class="organize-toolbar-label" data-i18n="modals.organize.categories_label">Categories:</label>
|
||
<select id="organize-cat-sort" class="organize-select">
|
||
<option value="manual" data-i18n="modals.organize.cat_manual">Manual Order</option>
|
||
<option value="az" data-i18n="modals.organize.cat_az">A → Z</option>
|
||
<option value="za" data-i18n="modals.organize.cat_za">Z → A</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="organize-channel-list" class="organize-list"></div>
|
||
|
||
<div class="organize-controls">
|
||
<button class="btn-sm" id="organize-move-up" data-i18n="modals.organize.move_up" data-i18n-title="modals.organize.move_up_title" title="Move selected channel up">⬆ Up</button>
|
||
<button class="btn-sm" id="organize-move-down" data-i18n="modals.organize.move_down" data-i18n-title="modals.organize.move_down_title" title="Move selected channel down">⬇ Down</button>
|
||
<div class="organize-spacer"></div>
|
||
<div class="organize-tag-group">
|
||
<input type="text" id="organize-tag-input" class="organize-tag-input" maxlength="30" data-i18n-placeholder="modals.organize.tag_placeholder" placeholder="Tag name…">
|
||
<button class="btn-sm organize-tag-btn" id="organize-set-tag" data-i18n-title="modals.organize.tag_set_title" title="Apply tag">🏷️ <span data-i18n="modals.organize.tag_set_btn">Set</span></button>
|
||
<button class="btn-sm organize-tag-btn danger" id="organize-remove-tag" data-i18n="modals.organize.tag_remove_btn" data-i18n-title="modals.organize.tag_remove_title" title="Remove tag">✕</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="organize-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div><!-- /modal-flex-body -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Move Channel Picker Modal -->
|
||
<div class="modal-overlay" id="reparent-modal" style="display:none">
|
||
<div class="modal" style="max-width:380px">
|
||
<h3 id="reparent-modal-title">📦 <span data-i18n="modals.move_channel.title">Move Channel</span></h3>
|
||
<p class="modal-desc" id="reparent-modal-desc"></p>
|
||
<div id="reparent-channel-list" class="organize-list" style="max-height:300px;overflow-y:auto"></div>
|
||
<div class="modal-actions" style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">
|
||
<button class="btn-sm" id="reparent-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DM Organize Modal -->
|
||
<div class="modal-overlay" id="dm-organize-modal" style="display:none">
|
||
<div class="modal" style="width:460px">
|
||
<h3>📋 <span data-i18n="modals.dm_organize.title">Organize DMs</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.dm_organize.desc">Tag your conversations into collapsible categories</p>
|
||
|
||
<div class="organize-toolbar">
|
||
<label class="organize-toolbar-label" data-i18n="modals.dm_organize.sort_label">Sort:</label>
|
||
<select id="dm-organize-sort" class="organize-select">
|
||
<option value="manual" data-i18n="modals.dm_organize.sort_manual">Manual</option>
|
||
<option value="alpha" data-i18n="modals.dm_organize.sort_alpha">Alphabetical</option>
|
||
<option value="recent" data-i18n="modals.dm_organize.sort_recent">Recent Activity</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="dm-organize-list" class="organize-list"></div>
|
||
|
||
<div class="organize-controls">
|
||
<button class="btn-sm" id="dm-organize-move-up" data-i18n="modals.organize.move_up" data-i18n-title="modals.organize.move_up_title" title="Move up">⬆ Up</button>
|
||
<button class="btn-sm" id="dm-organize-move-down" data-i18n="modals.organize.move_down" data-i18n-title="modals.organize.move_down_title" title="Move down">⬇ Down</button>
|
||
<div class="organize-spacer"></div>
|
||
<div class="organize-tag-group">
|
||
<input type="text" id="dm-organize-tag-input" class="organize-tag-input" maxlength="30" data-i18n-placeholder="modals.dm_organize.category_placeholder" placeholder="Category…">
|
||
<button class="btn-sm organize-tag-btn" id="dm-organize-set-tag" data-i18n-title="modals.dm_organize.category_set_title" title="Apply category">🏷️ <span data-i18n="modals.organize.tag_set_btn">Set</span></button>
|
||
<button class="btn-sm organize-tag-btn danger" id="dm-organize-remove-tag" data-i18n="modals.organize.tag_remove_btn" data-i18n-title="modals.dm_organize.category_remove_title" title="Remove category">✕</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="dm-organize-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Webhook Management Modal -->
|
||
|
||
<!-- Poll Creation Modal -->
|
||
<div class="modal-overlay" id="poll-modal" style="display:none">
|
||
<div class="modal" style="width:440px">
|
||
<h3>📊 <span data-i18n="modals.poll.title">Create Poll</span></h3>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.poll.question_label">Question</label>
|
||
<input type="text" id="poll-question-input" data-i18n-placeholder="modals.poll.question_placeholder" placeholder="What do you want to ask?" maxlength="300" autocomplete="off">
|
||
</div>
|
||
<div class="form-group" style="margin-top:8px">
|
||
<label data-i18n="modals.poll.options_label">Options</label>
|
||
<div id="poll-options-list" class="poll-options-list">
|
||
<div class="poll-option-row"><input type="text" class="poll-option-input" data-i18n-placeholder="modals.poll.option_1" placeholder="Option 1" maxlength="100"><button class="poll-option-remove" data-i18n-title="modals.poll.remove_option" title="Remove" style="display:none">×</button></div>
|
||
<div class="poll-option-row"><input type="text" class="poll-option-input" data-i18n-placeholder="modals.poll.option_2" placeholder="Option 2" maxlength="100"><button class="poll-option-remove" data-i18n-title="modals.poll.remove_option" title="Remove" style="display:none">×</button></div>
|
||
</div>
|
||
<button class="btn-sm" id="poll-add-option-btn" style="margin-top:6px" data-i18n="modals.poll.add_option_btn">+ Add Option</button>
|
||
</div>
|
||
<div class="form-group poll-settings-row" style="margin-top:10px">
|
||
<label class="poll-checkbox-label"><input type="checkbox" id="poll-multi-vote"> <span data-i18n="modals.poll.multi_vote">Allow multiple votes</span></label>
|
||
<label class="poll-checkbox-label"><input type="checkbox" id="poll-anonymous"> <span data-i18n="modals.poll.anonymous">Anonymous votes</span></label>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="poll-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="poll-create-btn" data-i18n="modals.poll.create_btn">Create Poll</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Timestamp Modal (/time with no args) -->
|
||
<div class="modal-overlay" id="time-modal" style="display:none">
|
||
<div class="modal tsm-modal" style="width:400px">
|
||
<div class="tsm-header">
|
||
<div class="tsm-clock" aria-hidden="true">🕐</div>
|
||
<p class="modal-desc tsm-subtitle" data-i18n="commands.description.time">Insert a time everyone sees in their own timezone</p>
|
||
<p class="tsm-syntax" data-i18n="commands.time_usage">Usage: /time 8pm</p>
|
||
</div>
|
||
|
||
<!-- Date row -->
|
||
<div class="tsm-row" role="group" data-i18n-aria-label="modals.time.date_label" aria-label="Date">
|
||
<div class="tsm-field">
|
||
<label for="tsm-year" data-i18n="modals.time.year">Year</label>
|
||
<input type="number" id="tsm-year" class="tsm-input" min="1970" max="9999" step="1" inputmode="numeric" autocomplete="off">
|
||
</div>
|
||
<div class="tsm-field">
|
||
<label for="tsm-month" data-i18n="modals.time.month">Month</label>
|
||
<input type="number" id="tsm-month" class="tsm-input" min="1" max="12" step="1" inputmode="numeric" autocomplete="off">
|
||
</div>
|
||
<div class="tsm-field">
|
||
<label for="tsm-day" data-i18n="modals.time.day">Day</label>
|
||
<input type="number" id="tsm-day" class="tsm-input" min="1" max="31" step="1" inputmode="numeric" autocomplete="off">
|
||
</div>
|
||
<div class="tsm-field tsm-cal-field">
|
||
<label aria-hidden="true"> </label>
|
||
<button type="button" id="tsm-cal-btn" class="tsm-cal-btn" data-i18n-title="modals.time.calendar" title="Open calendar">📅</button>
|
||
<input type="date" id="tsm-cal-input" class="tsm-cal-native" tabindex="-1" aria-hidden="true">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Time row -->
|
||
<div class="tsm-row" role="group" data-i18n-aria-label="modals.time.time_label" aria-label="Time">
|
||
<div class="tsm-field">
|
||
<label for="tsm-hour" data-i18n="modals.time.hour">Hour</label>
|
||
<input type="number" id="tsm-hour" class="tsm-input" min="0" max="23" step="1" inputmode="numeric" autocomplete="off">
|
||
</div>
|
||
<div class="tsm-field">
|
||
<label for="tsm-minute" data-i18n="modals.time.minute">Minute</label>
|
||
<input type="number" id="tsm-minute" class="tsm-input" min="0" max="59" step="1" inputmode="numeric" autocomplete="off">
|
||
</div>
|
||
<div class="tsm-field">
|
||
<label for="tsm-second" data-i18n="modals.time.second">Second</label>
|
||
<input type="number" id="tsm-second" class="tsm-input" min="0" max="59" step="1" inputmode="numeric" autocomplete="off">
|
||
</div>
|
||
<div class="tsm-field tsm-meridiem-field">
|
||
<label aria-hidden="true"> </label>
|
||
<div class="tsm-meridiem" role="group" data-i18n-aria-label="modals.time.clock_mode" aria-label="Clock mode">
|
||
<button type="button" class="tsm-mer-btn" data-mer="24" data-i18n="modals.time.hour_24">24HR</button>
|
||
<button type="button" class="tsm-mer-btn" data-mer="AM" data-i18n="modals.time.am">AM</button>
|
||
<button type="button" class="tsm-mer-btn" data-mer="PM" data-i18n="modals.time.pm">PM</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Styles: every format previewed, each with its own insert -->
|
||
<div class="tsm-styles" id="tsm-styles"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel Roles Modal -->
|
||
<div class="modal-overlay" id="channel-roles-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>👑 <span data-i18n="modals.channel_roles.title">Channel Roles</span></h3>
|
||
<p class="modal-desc" id="channel-roles-channel-name"></p>
|
||
|
||
<div class="channel-roles-layout">
|
||
<!-- Left: Members + Assignment -->
|
||
<div class="channel-roles-left">
|
||
<h4 class="channel-roles-section-title" data-i18n="modals.channel_roles.members_title">Members</h4>
|
||
<div class="channel-roles-member-list" id="channel-roles-member-list"></div>
|
||
|
||
<div class="channel-roles-actions" id="channel-roles-actions" style="display:none">
|
||
<h4 class="channel-roles-selected-name" id="channel-roles-selected-name"></h4>
|
||
<div class="channel-roles-current" id="channel-roles-current-roles"></div>
|
||
<div class="channel-roles-assign-area">
|
||
<div class="channel-roles-assign-row">
|
||
<select id="channel-roles-role-select" class="organize-select">
|
||
<option value="" data-i18n="modals.channel_roles.select_role">-- Select Role --</option>
|
||
</select>
|
||
<select id="channel-roles-scope-select" class="organize-select" style="max-width:140px">
|
||
<option value="server" data-i18n="modals.channel_roles.server_wide">🌐 Server-wide</option>
|
||
<option value="channel" data-i18n="modals.channel_roles.this_channel">📌 This Channel</option>
|
||
</select>
|
||
<button class="btn-sm btn-accent" id="channel-roles-assign-btn" data-i18n="modals.channel_roles.assign_btn">Assign</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Role Configuration -->
|
||
<div class="channel-roles-right">
|
||
<h4 class="channel-roles-section-title" data-i18n="modals.channel_roles.role_config_title">Role Configuration</h4>
|
||
<div class="channel-roles-role-list" id="channel-roles-role-list"></div>
|
||
<button class="btn-sm btn-accent btn-full" id="channel-roles-create-btn" data-i18n="modals.channel_roles.new_role_btn" style="margin-top:6px">+ New Role</button>
|
||
<div class="channel-roles-role-detail" id="channel-roles-role-detail">
|
||
<p class="muted-text" style="padding:12px;text-align:center;font-size:0.82rem" data-i18n="modals.channel_roles.select_to_configure">Select a role to configure</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="channel-roles-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="webhook-modal" style="display:none">
|
||
<div class="modal" style="width:520px">
|
||
<h3>🤖 <span data-i18n="modals.webhook.title">Webhooks</span></h3>
|
||
<p class="modal-desc" id="webhook-modal-channel-name"></p>
|
||
|
||
<!-- Create new webhook -->
|
||
<div class="form-group" style="display:flex;gap:8px;align-items:flex-end">
|
||
<div style="flex:1">
|
||
<label data-i18n="modals.webhook.name_label">New Webhook Name</label>
|
||
<input type="text" id="webhook-name-input" class="input" maxlength="32" data-i18n-placeholder="modals.webhook.name_placeholder" placeholder="My Bot" style="width:100%">
|
||
</div>
|
||
<button class="btn-sm btn-accent" id="webhook-create-btn" data-i18n="modals.webhook.create_btn" style="height:36px">Create</button>
|
||
</div>
|
||
|
||
<!-- Webhook list -->
|
||
<div id="webhook-list" style="margin-top:14px;max-height:280px;overflow-y:auto"></div>
|
||
|
||
<!-- Token reveal (shown once after creation) -->
|
||
<div id="webhook-token-reveal" style="display:none;margin-top:14px;padding:12px;border-radius:8px;background:rgba(0,0,0,0.25);border:1px solid var(--accent-color, #00e5ff)">
|
||
<p style="font-size:0.85rem;margin:0 0 8px;opacity:0.7">⚠️ <span data-i18n="modals.webhook.token_warning">Copy this URL now — you won't see the full token again!</span></p>
|
||
<div style="display:flex;gap:6px">
|
||
<input type="text" id="webhook-url-display" class="input" readonly style="flex:1;font-size:0.8rem;font-family:monospace">
|
||
<button class="btn-sm" id="webhook-copy-url-btn">📋 <span data-i18n="modals.webhook.copy_btn">Copy</span></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top:16px">
|
||
<button class="btn-sm" id="webhook-close-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sub-channel Subscriptions Panel -->
|
||
<div class="modal-overlay" id="sub-panel-modal" style="display:none">
|
||
<div class="modal" style="width:520px;max-height:80vh;display:flex;flex-direction:column">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<h3 style="margin:0;flex:1">📡 <span data-i18n="modals.sub_panel.title">Sub-channel Subscriptions</span></h3>
|
||
<button class="btn-sm" id="sub-panel-close-btn" data-i18n="modals.common.close" style="font-size:1rem;padding:4px 10px">×</button>
|
||
</div>
|
||
<p class="modal-desc" style="margin:0 0 12px;font-size:0.8rem;opacity:0.6" data-i18n="modals.sub_panel.desc">Subscribe to get notifications. Unsubscribed channels still appear but won't notify you.</p>
|
||
<div id="sub-panel-content" style="overflow-y:auto;flex:1;min-height:0"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hidden Channels Modal (admin declutter via "Hide Channel") -->
|
||
<div class="modal-overlay" id="hidden-channels-modal" style="display:none">
|
||
<div class="modal" style="width:460px;max-height:80vh;display:flex;flex-direction:column">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<h3 style="margin:0;flex:1">🙈 <span data-i18n="modals.hidden_channels.title">Hidden Channels</span></h3>
|
||
<button class="btn-sm" id="hidden-channels-close-btn" data-i18n="modals.common.close" style="font-size:1rem;padding:4px 10px">×</button>
|
||
</div>
|
||
<p class="modal-desc" style="margin:0 0 12px;font-size:0.8rem;opacity:0.6" data-i18n="modals.hidden_channels.desc">Hidden channels are removed from your sidebar but you still have full access to them. Unhide one to bring it back.</p>
|
||
<div id="hidden-channels-content" style="overflow-y:auto;flex:1;min-height:0"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Sub-channel Modal -->
|
||
<div class="modal-overlay" id="create-sub-modal" style="display:none">
|
||
<div class="modal" style="max-width:380px">
|
||
<h3 data-i18n="modals.create_sub.title">Create Sub-channel</h3>
|
||
<p class="modal-desc" id="create-sub-parent-name"></p>
|
||
<label style="display:block;margin:12px 0 6px;font-size:0.85rem;opacity:0.7" data-i18n="modals.create_sub.name_label">Sub-channel name</label>
|
||
<input type="text" id="create-sub-name" class="input" maxlength="50" data-i18n-placeholder="modals.create_sub.name_placeholder" placeholder="sub-channel name..." style="width:100%">
|
||
<label class="checkbox-row" style="margin:14px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer">
|
||
<input type="checkbox" id="create-sub-private">
|
||
<span style="font-size:0.9rem">🔒 <span data-i18n="modals.create_sub.private_label">Private</span> <span style="opacity:0.5;font-size:0.8rem" data-i18n="modals.create_sub.private_hint">(invite-only, hidden from non-members)</span></span>
|
||
</label>
|
||
<label class="checkbox-row" style="margin:2px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.9rem">
|
||
<input type="checkbox" id="create-sub-temporary">
|
||
<span>⏱️ <span data-i18n="modals.create_sub.temporary_label">Temporary</span> <span style="opacity:0.5;font-size:0.8rem" data-i18n="modals.create_sub.temporary_hint">(auto-delete)</span></span>
|
||
</label>
|
||
<div id="sub-temp-duration-row" style="display:none;margin:0 0 8px;padding-left:24px">
|
||
<div style="display:flex;gap:6px;align-items:center">
|
||
<input type="number" id="create-sub-duration" min="1" max="720" value="24" style="width:60px;font-size:0.8rem" data-i18n-placeholder="modals.create_sub.hours_placeholder" placeholder="Hours">
|
||
<span style="font-size:0.75rem;opacity:0.6" data-i18n="modals.create_sub.hours">hours</span>
|
||
</div>
|
||
</div>
|
||
<p class="muted-text" style="font-size:0.75rem;margin:0 0 8px;opacity:0.6">ℹ️ <span data-i18n="modals.create_sub.admin_note">Admins always have access to all channels, including private ones.</span></p>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
|
||
<button class="btn-sm" id="create-sub-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="create-sub-confirm-btn" data-i18n="modals.create_sub.create_btn">Create</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stackable effect overlay container -->
|
||
<div id="fx-layers"></div>
|
||
|
||
<!-- Push Notification Error Modal (center-screen, requires acknowledgement) -->
|
||
<!-- Activities / Games Launcher Modal -->
|
||
<div class="modal-overlay" id="activities-modal" style="display:none">
|
||
<div class="modal modal-activities">
|
||
<div class="activities-header">
|
||
<h3>🎮 <span data-i18n="modals.activities.title">Activities</span></h3>
|
||
<button class="settings-close-btn" id="close-activities-btn" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
</div>
|
||
<div class="activities-grid" id="activities-grid">
|
||
<!-- Populated dynamically by JS from GAMES_REGISTRY -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Iframe Game Container (fullscreen overlay) -->
|
||
<div class="game-iframe-overlay" id="game-iframe-overlay" style="display:none">
|
||
<div class="game-iframe-header">
|
||
<span class="game-iframe-title" id="game-iframe-title" data-i18n="modals.game_overlay.game">Game</span>
|
||
<div class="game-iframe-actions">
|
||
<label style="display:flex;align-items:center;gap:6px;color:var(--text-secondary);font-size:0.75rem;margin-right:8px;" data-i18n-title="modals.game_overlay.volume_label" title="Game Volume">
|
||
🔊 <input type="range" id="game-volume-slider" min="0" max="100" value="80" style="width:80px;accent-color:var(--accent);">
|
||
<span id="game-volume-pct" style="min-width:28px;font-size:0.6875rem;">80%</span>
|
||
</label>
|
||
<button class="btn-sm" id="game-iframe-popout" data-i18n-title="modals.game_overlay.popout_title" title="Open in new window">↗️ <span data-i18n="modals.game_overlay.popout_btn">Pop Out</span></button>
|
||
<button class="btn-sm btn-danger" id="game-iframe-close" data-i18n="modals.game_overlay.close_btn" data-i18n-title="modals.game_overlay.close_title" title="Close game">× Close</button>
|
||
</div>
|
||
</div>
|
||
<iframe id="game-iframe" class="game-iframe" sandbox="allow-scripts allow-same-origin allow-popups allow-forms" allowfullscreen></iframe>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="push-error-modal" style="display:none">
|
||
<div class="modal" style="width:420px;text-align:center;padding:28px 24px;">
|
||
<div style="font-size:3rem;margin-bottom:12px;">🔕</div>
|
||
<h3 style="margin:0 0 12px;color:var(--text-primary)" data-i18n="modals.push_error.title">Push Notifications Unavailable</h3>
|
||
<p id="push-error-reason" style="color:var(--text-secondary);font-size:0.875rem;line-height:1.5;margin:0 0 16px;"></p>
|
||
<p style="color:var(--text-muted);font-size:0.75rem;line-height:1.5;margin:0 0 20px;" data-i18n-html="modals.push_error.recommendation">
|
||
<strong>Recommended:</strong> Google Chrome, Microsoft Edge, or Opera on desktop.<br>
|
||
iOS requires Safari 16.4+ with Haven added to your Home Screen.
|
||
</p>
|
||
<button class="btn-accent" id="push-error-dismiss-btn" style="width:100%;padding:10px;font-size:0.875rem;font-weight:600;" data-i18n="modals.push_error.got_it">Got It</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E2E Password Prompt Modal -->
|
||
<div id="e2e-password-modal" class="modal-overlay" style="display:none">
|
||
<div class="modal e2e-password-modal">
|
||
<h3>🔐 <span data-i18n="modals.e2e_password.title">Password Required</span></h3>
|
||
<p class="e2e-pw-desc" data-i18n="modals.e2e_password.desc">End-to-end encryption requires your password to unlock your keys. Enter your account password to continue.</p>
|
||
<div class="e2e-pw-field">
|
||
<input type="password" id="e2e-pw-input" data-i18n-placeholder="modals.e2e_password.placeholder" placeholder="Enter your password" autocomplete="current-password" spellcheck="false">
|
||
</div>
|
||
<div id="e2e-pw-error" class="e2e-pw-error" style="display:none"></div>
|
||
<div class="e2e-pw-actions">
|
||
<button class="btn-accent" id="e2e-pw-submit-btn" data-i18n="modals.e2e_password.unlock_btn">Unlock</button>
|
||
<button class="btn-sm" id="e2e-pw-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Desktop App Promo Popup -->
|
||
<!-- Android App Promo Modal -->
|
||
<div class="modal-overlay" id="android-beta-modal" style="display:none">
|
||
<div class="modal android-beta-promo">
|
||
<div class="android-beta-promo-icon">📱</div>
|
||
<span class="android-beta-promo-badge" data-i18n="modals.android_beta.badge" style="background:var(--accent-bright,#4caf50)">NOW AVAILABLE</span>
|
||
<h3 class="android-beta-promo-title"><span data-i18n="modals.android_beta.title">Amni-Haven Android</span></h3>
|
||
<p class="android-beta-promo-subtitle" data-i18n="modals.android_beta.subtitle">A native Android app for Haven, built from the ground up by Amnibro!</p>
|
||
<ul class="android-beta-promo-features">
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_native">Native Android experience</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_push">Push notifications via Google Play</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_chat">Full chat & voice support</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_built">Built from the ground up for Haven</span></li>
|
||
</ul>
|
||
<p class="android-beta-promo-note" data-i18n="modals.android_beta.note">Available now on Google Play:</p>
|
||
<div class="android-beta-form">
|
||
<a id="android-beta-submit" href="https://play.google.com/store/apps/details?id=com.havenapp.mobile&gl=US" target="_blank" rel="noopener" class="btn btn-accent android-beta-submit" style="text-decoration:none;display:inline-block">📲 <span data-i18n="modals.android_beta.request_btn">Get it on Google Play</span></a>
|
||
</div>
|
||
<p class="android-beta-promo-credit" data-i18n-html="modals.android_beta.credit">Built with ❤️ by <strong>Amnibro</strong> — thank you for your incredible work on the Amni-Haven Android app!</p>
|
||
<div class="android-beta-promo-actions">
|
||
<button id="android-beta-later" class="btn android-beta-promo-later" data-i18n="modals.android_beta.later_btn">Maybe later</button>
|
||
</div>
|
||
<label class="android-beta-promo-dismiss-label">
|
||
<input type="checkbox" id="android-beta-dismiss-check"> <span data-i18n="modals.android_beta.dont_show">Don't show this again</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="desktop-promo-modal" style="display:none">
|
||
<div class="modal desktop-promo">
|
||
<div class="desktop-promo-icon">🖥️</div>
|
||
<h3 class="desktop-promo-title"><span data-i18n="modals.desktop_promo.title">Haven Desktop App</span> <span class="desktop-promo-badge" data-i18n="modals.desktop_promo.badge">BETA</span></h3>
|
||
<p class="desktop-promo-subtitle" data-i18n="modals.desktop_promo.subtitle">Now available for your platform!</p>
|
||
<ul class="desktop-promo-features">
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_notifs">Native system notifications</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_tray">Minimize to system tray</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_audio">Per-app audio routing</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_launcher">Built-in server launcher</span></li>
|
||
</ul>
|
||
<p class="desktop-promo-meta" id="desktop-promo-meta">Windows Installer • v1.0.0</p>
|
||
<div class="desktop-promo-actions">
|
||
<a id="desktop-promo-install" class="btn btn-accent desktop-promo-install" href="https://ancsemi.github.io/Haven/#download" target="_blank" rel="noopener">⬇ <span data-i18n="modals.desktop_promo.install_btn">Install Haven</span></a>
|
||
<button id="desktop-promo-later" class="btn desktop-promo-later" data-i18n="modals.desktop_promo.later_btn">Maybe later</button>
|
||
</div>
|
||
<label class="desktop-promo-dismiss-label">
|
||
<input type="checkbox" id="desktop-promo-dismiss-check"> <span data-i18n="modals.desktop_promo.dont_show">Don't show this again</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Donors Thank You Modal -->
|
||
<div class="modal-overlay" id="donors-modal" style="display:none">
|
||
<div class="modal donors-modal-box">
|
||
<button class="donors-close-btn" id="donors-close-btn" data-i18n-title="modals.common.close" title="Close">×</button>
|
||
<div class="donors-heart-icon">❤️</div>
|
||
<h2 class="donors-title" data-i18n="modals.donors.title">Thank You</h2>
|
||
<p class="donors-subtitle" data-i18n="modals.donors.subtitle">Haven is built with love and kept alive by the generosity of people like you. Every donation — big or small — means the world.</p>
|
||
|
||
<div class="donors-sort-toggle" id="donors-sort-toggle" style="display:none">
|
||
<button class="donors-sort-btn active" data-sort="chronological" data-i18n="modals.donors.sort_chronological">Early Believers</button>
|
||
<button class="donors-sort-btn" data-sort="featured" data-i18n="modals.donors.sort_featured">Biggest Thanks!</button>
|
||
</div>
|
||
|
||
<div class="donors-tiers" id="donors-tiers">
|
||
<div class="donors-tier">
|
||
<h3 class="donors-tier-title donors-sponsor-title">✨ <span data-i18n="modals.donors.sponsors_title">Sponsors</span></h3>
|
||
<div class="donors-grid" id="sponsors-grid"></div>
|
||
</div>
|
||
|
||
<div class="donors-tier">
|
||
<h3 class="donors-tier-title donors-donor-title">💛 <span data-i18n="modals.donors.donors_title">Donors</span></h3>
|
||
<div class="donors-grid" id="donors-grid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="donors-note" data-i18n="modals.donors.note">New donors will be added each release. If you've donated and don't see your name, it'll be in the next update!</p>
|
||
|
||
<a href="https://ko-fi.com/ancsemi" target="_blank" rel="noopener noreferrer" class="donors-kofi-btn">
|
||
<span>☕ <span data-i18n="modals.donors.kofi_btn">Support Haven on Ko-fi</span></span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/i18n.js"></script>
|
||
<script src="/socket.io/socket.io.js"></script>
|
||
|
||
<!-- Image Lightbox Overlay -->
|
||
<div id="image-lightbox" class="image-lightbox" style="display:none">
|
||
<button id="lightbox-prev" class="lightbox-nav lightbox-prev" data-i18n-aria-label="media_gallery.previous_image" aria-label="Previous image">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||
</button>
|
||
<img id="lightbox-img" class="lightbox-img" src="" alt="">
|
||
<button id="lightbox-next" class="lightbox-nav lightbox-next" data-i18n-aria-label="media_gallery.next_image" aria-label="Next image">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Channel Media Gallery Modal (#5350) -->
|
||
<div class="modal-overlay" id="media-gallery-modal" style="display:none">
|
||
<div class="modal media-gallery-modal">
|
||
<div class="media-gallery-header">
|
||
<h3 data-i18n="media_gallery.title">Files & Media</h3>
|
||
<button class="icon-btn small" id="media-gallery-close" data-i18n-aria-label="modals.common.close" aria-label="Close">×</button>
|
||
</div>
|
||
<div class="media-gallery-tabs" role="tablist">
|
||
<button class="media-tab active" data-tab="photos" role="tab">🖼 <span data-i18n="media_gallery.photos">Photos</span> <span class="media-tab-count" id="media-count-photos">0</span></button>
|
||
<button class="media-tab" data-tab="videos" role="tab">🎬 <span data-i18n="media_gallery.videos">Videos</span> <span class="media-tab-count" id="media-count-videos">0</span></button>
|
||
<button class="media-tab" data-tab="audios" role="tab">🎵 <span data-i18n="media_gallery.audio">Audio</span> <span class="media-tab-count" id="media-count-audios">0</span></button>
|
||
<button class="media-tab" data-tab="files" role="tab">📄 <span data-i18n="media_gallery.files">Files</span> <span class="media-tab-count" id="media-count-files">0</span></button>
|
||
<button class="media-tab" data-tab="links" role="tab">🔗 <span data-i18n="media_gallery.links">Links</span> <span class="media-tab-count" id="media-count-links">0</span></button>
|
||
</div>
|
||
<div class="media-gallery-toolbar">
|
||
<label class="media-gallery-sort-label">
|
||
<span data-i18n="media_gallery.sort_by">Sort by</span>
|
||
<select id="media-gallery-sort" class="media-gallery-sort">
|
||
<option value="date-desc" data-i18n="media_gallery.sort_newest">Newest first</option>
|
||
<option value="date-asc" data-i18n="media_gallery.sort_oldest">Oldest first</option>
|
||
<option value="size-desc" data-i18n="media_gallery.sort_largest">Largest first</option>
|
||
<option value="size-asc" data-i18n="media_gallery.sort_smallest">Smallest first</option>
|
||
<option value="name-asc" data-i18n="media_gallery.sort_name_az">Name A→Z</option>
|
||
<option value="name-desc" data-i18n="media_gallery.sort_name_za">Name Z→A</option>
|
||
</select>
|
||
</label>
|
||
<div class="media-gallery-actions" id="media-gallery-actions" style="display:none">
|
||
<button class="btn-sm" id="media-gallery-select-toggle" data-i18n="media_gallery.select">Select</button>
|
||
<span class="media-gallery-selection-info" id="media-gallery-selection-info" style="display:none"></span>
|
||
<button class="btn-sm" id="media-gallery-select-all" style="display:none" data-i18n="media_gallery.select_all">Select all</button>
|
||
<button class="btn-sm btn-danger-fill" id="media-gallery-delete" style="display:none" disabled data-i18n="media_gallery.delete_selected">Delete selected</button>
|
||
</div>
|
||
</div>
|
||
<div class="media-gallery-body" id="media-gallery-body">
|
||
<div class="media-gallery-empty muted-text" data-i18n="media_gallery.loading">Loading…</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-overlay" id="threads-list-modal" style="display:none">
|
||
<div class="modal media-gallery-modal">
|
||
<div class="media-gallery-header">
|
||
<h3 data-i18n="thread_list.title">Threads</h3>
|
||
<button class="icon-btn small" id="threads-list-close" data-i18n-aria-label="modals.common.close" aria-label="Close">×</button>
|
||
</div>
|
||
<div class="media-gallery-toolbar">
|
||
<input type="text" id="threads-list-search" class="thread-list-search"
|
||
data-i18n-placeholder="thread_list.search_placeholder"
|
||
placeholder="Search threads…" autocomplete="off">
|
||
<span class="thread-list-count muted-text" id="threads-list-count"></span>
|
||
</div>
|
||
<div class="media-gallery-body" id="threads-list-body">
|
||
<div class="media-gallery-empty muted-text" data-i18n="thread_list.loading">Loading…</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="/js/theme.js?v=4.1.1"></script>
|
||
<script src="/js/notifications.js?v=3.16.16"></script>
|
||
<script src="/js/servers.js?v=3.16.15"></script>
|
||
<script src="/js/voice.js?v=3.34.7"></script>
|
||
<script src="/js/modmode.js?v=3.16.16"></script>
|
||
<script src="/js/e2e.js?v=3.16.15"></script>
|
||
<!-- Shared with the server (src/automod.js requires the same file) so both
|
||
sides reach identical verdicts on a link. Must load before app.js. -->
|
||
<script src="/js/automod-rules.js?v=3.44.0"></script>
|
||
<script type="module" src="/js/app.js?v=4.1.3"></script>
|
||
<script src="/js/plugin-loader.js?v=4.1.2"></script>
|
||
<script src="/js/modules/stream-debug.js?v=3.16.15"></script>
|
||
<script src="/js/password-eye.js?v=3.17.1"></script>
|
||
</body>
|
||
</html>
|