Files
Haven/public/app.html
T
ancsemi 31644c4d39 Voice UX polish, message pinning, markdown, link previews
- Voice join/leave audio cues (ascending/descending synth tones)
- Talking indicator: username glows with accent pulse when speaking
- Multi-stream screen sharing: CSS Grid tile layout, per-user labels
- Fix remoteStream variable reference bug in voice.js _createPeer
- Message pinning (admin-only): pin/unpin with DB table, panel, 50-pin limit
- Enhanced markdown: fenced code blocks, blockquotes, improved inline code
- Link previews: server-side OG metadata fetch with 30-min cache, preview cards
- CSP updated for preview images
- .gitignore: exclude .vs/
2026-02-11 19:18:30 -05:00

469 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<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">
<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">
<title>Haven</title>
<link rel="stylesheet" href="/css/style.css">
<script src="/js/theme-init.js"></script>
</head>
<body>
<div id="app">
<div id="app-body">
<!-- ─── Server Bar (far left — like Discord) ─────── -->
<nav class="server-bar" id="server-bar">
<div class="server-icon home active" id="home-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"></div>
<button class="server-icon add-server" id="add-server-btn" title="Add Server">
<span class="server-icon-text">+</span>
</button>
</nav>
<!-- ─── Left Sidebar ────────────────────────────── -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="brand">
<span class="logo-sm">⬡</span>
<span class="brand-text">HAVEN</span>
</div>
<div class="user-bar">
<span class="led on" id="connection-led" title="Server connection"></span>
<span class="current-user" id="current-user"></span>
<button id="rename-btn" class="icon-btn small" title="Change username">✏️</button>
<button id="logout-btn" class="icon-btn" title="Logout">⏻</button>
</div>
</div>
<div class="sidebar-section">
<h5 class="section-label">Join a Channel</h5>
<div class="input-row">
<input type="text" id="channel-code-input"
placeholder="Enter code..." maxlength="8" spellcheck="false">
<button id="join-channel-btn" class="btn-sm">Join</button>
</div>
</div>
<div class="sidebar-section" id="admin-controls" style="display:none">
<h5 class="section-label">Create Channel</h5>
<div class="input-row">
<input type="text" id="new-channel-name"
placeholder="Channel name..." maxlength="50">
<button id="create-channel-btn" class="btn-sm btn-accent">+</button>
</div>
</div>
<div class="sidebar-section channel-section">
<h5 class="section-label">Channels</h5>
<div class="channel-list" id="channel-list"></div>
</div>
<!-- Theme Selector -->
<div class="sidebar-section theme-sidebar-section" style="margin-top:auto">
<h5 class="section-label">Theme</h5>
<div class="theme-selector" id="theme-selector">
<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="ice" title="Ice"><span class="theme-icon">🧊</span></button>
<button class="theme-btn" data-theme="abyss" title="Abyss"><span class="theme-icon">🌊</span></button>
</div>
<button class="btn-game-sidebar" id="play-flappy-btn" title="Play Shippy Container">� Shippy Container</button>
</div>
</aside>
<!-- ─── Main Content ────────────────────────────── -->
<main class="main">
<header class="channel-header">
<button id="mobile-menu-btn" class="mobile-menu-btn" 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="channel-info">
<h3 id="channel-header-name">Select a channel</h3>
<span class="channel-code-tag" id="channel-code-display" title="Channel code"></span>
<button id="copy-code-btn" class="icon-btn small" title="Copy code"
style="display:none">📋</button>
<button id="delete-channel-btn" class="icon-btn small danger" title="Delete channel"
style="display:none">🗑️</button>
</div>
<div class="voice-controls">
<div class="search-container" id="search-container" style="display:none">
<input type="text" id="search-input" class="search-input" placeholder="Search messages..." maxlength="100">
<button id="search-close-btn" class="icon-btn small" title="Close search">&times;</button>
</div>
<button id="search-toggle-btn" class="icon-btn small" title="Search messages (Ctrl+F)" style="display:none">🔍</button>
<button id="pinned-toggle-btn" class="icon-btn small" title="Pinned messages" style="display:none">📌</button>
<button id="mobile-users-btn" class="mobile-users-btn" 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>
<button id="voice-join-btn" class="btn-voice" style="display:none">
🎤 Join Voice
</button>
<button id="voice-mute-btn" class="btn-voice" style="display:none">
🔇 Mute
</button>
<button id="voice-deafen-btn" class="btn-voice" style="display:none">
🔇 Deafen
</button>
<button id="screen-share-btn" class="btn-voice" style="display:none" title="Share Screen">
🖥️ Share
</button>
<button id="voice-leave-btn" class="btn-voice btn-danger" style="display:none">
📞 Leave
</button>
</div>
</header>
<!-- Welcome Screen -->
<div id="no-channel-msg" class="welcome-screen">
<div class="welcome-content">
<div class="welcome-icon">⬡</div>
<h2>Welcome to Haven</h2>
<p>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" style="display:none">
<!-- Screen Share Viewer (multi-stream) -->
<div id="screen-share-container" class="screen-share-container" style="display:none">
<div class="screen-share-header">
<span id="screen-share-label">🖥️ Streams</span>
<button id="screen-share-close" class="icon-btn small" title="Minimize">&times;</button>
</div>
<div id="screen-share-grid" class="screen-share-grid"></div>
</div>
<div id="search-results-panel" class="search-results-panel" style="display:none">
<div class="search-results-header">
<span id="search-results-count">Results</span>
<button id="search-results-close" class="icon-btn small">&times;</button>
</div>
<div id="search-results-list" class="search-results-list"></div>
</div>
<div id="pinned-panel" class="pinned-panel" style="display:none">
<div class="pinned-panel-header">
<span id="pinned-count">📌 Pinned Messages</span>
<button id="pinned-close" class="icon-btn small">&times;</button>
</div>
<div id="pinned-list" class="pinned-list"></div>
</div>
<div class="messages" id="messages"></div>
<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" title="Cancel reply">&times;</button>
</div>
<div class="message-input-area">
<button id="upload-btn" class="btn-upload" title="Upload Image">
<svg 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>
</button>
<button id="emoji-btn" class="btn-emoji" title="Emoji">😀</button>
<div id="emoji-picker" class="emoji-picker" style="display:none"></div>
<button id="gif-btn" class="btn-gif" title="Search GIFs">GIF</button>
<div id="gif-picker" class="gif-picker" style="display:none">
<div class="gif-picker-header">
<input type="text" id="gif-search-input" placeholder="Search Tenor..." maxlength="100" autocomplete="off">
</div>
<div class="gif-picker-grid" id="gif-grid"></div>
<div class="gif-picker-footer">Powered by Tenor</div>
</div>
<div id="mention-dropdown" class="mention-dropdown" style="display:none"></div>
<div id="slash-dropdown" class="slash-dropdown" style="display:none"></div>
<textarea id="message-input" placeholder="Type a message... (/ for commands)"
rows="1" maxlength="2000"></textarea>
<button id="send-btn" class="btn-send" 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" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
</div>
</main>
<!-- ─── Right Sidebar ───────────────────────────── -->
<aside class="right-sidebar" id="right-sidebar">
<div class="panel">
<h5 class="panel-title">🔊 Voice</h5>
<div id="voice-users" class="user-list">
<p class="muted-text">No one in voice</p>
</div>
</div>
<div class="panel">
<h5 class="panel-title">👥 Online</h5>
<div id="online-users" class="user-list">
<p class="muted-text">Select a channel</p>
</div>
</div>
<div class="panel sidebar-settings-panel">
<button class="btn-settings-popout" id="open-settings-btn" 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>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">
<div class="status-item">
<span class="led on" id="status-server-led"></span>
<span class="label">Server</span>
<span class="value" id="status-server-text">Connected</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="led off" id="status-voice-led"></span>
<span class="label">Voice</span>
<span class="value" id="status-voice-text">Off</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="label">Ping</span>
<span class="value" id="status-ping">--</span>
<span class="label">ms</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="label">Channel</span>
<span class="value" id="status-channel">None</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="label">Online</span>
<span class="value" id="status-online-count">0</span>
</div>
<span class="spacer"></span>
<div class="status-item">
<span class="value" id="status-clock"></span>
</div>
</div>
</div>
<!-- Hidden audio container for voice streams -->
<div id="audio-container" style="display:none"></div>
<!-- Toast notifications -->
<div id="toast-container"></div>
<!-- Add Server Modal -->
<div class="modal-overlay" id="add-server-modal" style="display:none">
<div class="modal">
<h3>Add a Server</h3>
<p class="modal-desc">Connect to a friend's Haven server</p>
<div class="form-group">
<label>Server Name</label>
<input type="text" id="server-name-input" placeholder='e.g. Jake&apos;s Haven' maxlength="30">
</div>
<div class="form-group">
<label>Server Address</label>
<input type="text" id="server-url-input" placeholder="e.g. https://192.168.1.5:3000">
</div>
<div class="modal-actions">
<button class="btn-sm" id="cancel-server-btn">Cancel</button>
<button class="btn-sm btn-accent" id="save-server-btn">Add Server</button>
</div>
</div>
</div>
<!-- Rename Modal -->
<div class="modal-overlay" id="rename-modal" style="display:none">
<div class="modal">
<h3>Change Username</h3>
<p class="modal-desc">Letters, numbers, and underscores only (3–20 chars)</p>
<div class="form-group">
<label>New Username</label>
<input type="text" id="rename-input" placeholder="New username..." maxlength="20">
</div>
<div class="modal-actions">
<button class="btn-sm" id="cancel-rename-btn">Cancel</button>
<button class="btn-sm btn-accent" id="save-rename-btn">Save</button>
</div>
</div>
</div>
<!-- Admin Action Modal (Kick/Ban/Mute) -->
<div class="modal-overlay" id="admin-action-modal" style="display:none">
<div class="modal">
<h3 id="admin-action-title">Moderate User</h3>
<p class="modal-desc" id="admin-action-desc"></p>
<div class="form-group" id="admin-reason-group">
<label>Reason (optional)</label>
<input type="text" id="admin-action-reason" placeholder="Reason..." maxlength="200">
</div>
<div class="form-group" id="admin-duration-group" style="display:none">
<label>Duration (minutes)</label>
<input type="number" id="admin-action-duration" value="10" min="1" max="43200" step="1">
</div>
<div class="modal-actions">
<button class="btn-sm" id="cancel-admin-action-btn">Cancel</button>
<button class="btn-sm btn-accent btn-danger-fill" id="confirm-admin-action-btn">Confirm</button>
</div>
</div>
</div>
<!-- Settings Popout Modal -->
<div class="modal-overlay" id="settings-modal" style="display:none">
<div class="modal modal-settings">
<div class="settings-header">
<h3>⚙️ Settings</h3>
<button class="settings-close-btn" id="close-settings-btn">&times;</button>
</div>
<!-- Sounds Section -->
<div class="settings-section">
<h5 class="settings-section-title">🔔 Sounds</h5>
<div class="notif-settings">
<label class="toggle-row">
<span>Notifications</span>
<input type="checkbox" id="notif-enabled" checked>
</label>
<label class="volume-row">
<span>Volume</span>
<input type="range" id="notif-volume" min="0" max="100" value="50" class="slider-sm">
</label>
<label class="select-row">
<span>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>
<div class="notif-divider"></div>
<label class="volume-row">
<span>@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>@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>
</div>
<!-- Admin Section (hidden for non-admins) -->
<div class="settings-section admin-settings-section" id="admin-mod-panel" style="display:none">
<h5 class="settings-section-title">🛡️ Admin</h5>
<div class="admin-settings">
<label class="select-row">
<span>Show Members</span>
<select id="member-visibility-select">
<option value="online">Online Only</option>
<option value="all">All Members</option>
<option value="none">Hidden</option>
</select>
</label>
<button class="btn-sm btn-full" id="view-bans-btn">📋 View Bans</button>
</div>
<div class="admin-settings" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">� Whitelist</h5>
<label class="toggle-row">
<span>Enabled</span>
<input type="checkbox" id="whitelist-enabled">
</label>
<small class="settings-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" placeholder="Username..." maxlength="20" class="settings-text-input">
<button class="btn-sm btn-accent" id="whitelist-add-btn">Add</button>
</div>
<div id="whitelist-list" class="whitelist-list">
<p class="muted-text">Loading...</p>
</div>
</div>
</div>
<div class="admin-settings" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">�🗑️ Auto-Cleanup</h5>
<label class="toggle-row">
<span>Enabled</span>
<input type="checkbox" id="cleanup-enabled">
</label>
<label class="select-row">
<span>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">Delete messages older than N days (0 = off)</small>
<label class="select-row">
<span>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">Trim oldest messages when DB exceeds N MB (0 = off)</small>
<button class="btn-sm btn-full" id="run-cleanup-now-btn" style="margin-top:4px;">🧹 Run Cleanup Now</button>
</div>
</div>
</div>
</div>
<!-- Bans List Modal -->
<div class="modal-overlay" id="bans-modal" style="display:none">
<div class="modal modal-wide">
<h3>Banned Users</h3>
<div id="bans-list" class="bans-list">
<p class="muted-text">Loading...</p>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-bans-btn">Close</button>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/theme.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/servers.js"></script>
<script src="/js/voice.js"></script>
<script src="/js/app.js"></script>
</body>
</html>