From 631deb62c1ddd178e82ed1774d0117a11ac5c999 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 18 Feb 2026 15:55:37 +0000 Subject: [PATCH] Autosave: 20260218-155536 --- api_v1_voice.php | 224 ++++----- assets/js/main.js | 435 +++++++++++------- assets/js/voice.js | 34 +- assets/pasted-20260218-144808-f4591d31.png | Bin 0 -> 8843 bytes assets/pasted-20260218-153932-fac8d7bf.png | Bin 0 -> 39235 bytes data/22.log | 0 data/22.participants.json | 1 - data/3.log | 4 - data/3.participants.json | 1 - data/6.log | 10 - data/6.participants.json | 1 - data/test.participants.json | 1 - db/migrations/20260218_voice_db_signaling.sql | 17 + index.php | 3 +- requests.log | 49 ++ 15 files changed, 440 insertions(+), 340 deletions(-) create mode 100644 assets/pasted-20260218-144808-f4591d31.png create mode 100644 assets/pasted-20260218-153932-fac8d7bf.png delete mode 100644 data/22.log delete mode 100644 data/22.participants.json delete mode 100644 data/3.log delete mode 100644 data/3.participants.json delete mode 100644 data/6.log delete mode 100644 data/6.participants.json delete mode 100644 data/test.participants.json create mode 100644 db/migrations/20260218_voice_db_signaling.sql diff --git a/api_v1_voice.php b/api_v1_voice.php index b36d663..fc3a4b1 100644 --- a/api_v1_voice.php +++ b/api_v1_voice.php @@ -1,5 +1,4 @@ - 0 && count($lines) < $maxLines) { - $readSize = min($pos, 4096); - $pos -= $readSize; - fseek($fp, $pos); - $chunk = fread($fp, $readSize); - $buffer = $chunk . $buffer; - - $chunkLines = explode("\n", $buffer); - $buffer = array_shift($chunkLines); - - while (!empty($chunkLines)) { - $line = array_pop($chunkLines); - if (trim($line) !== "") { - array_unshift($lines, trim($line)); - if (count($lines) >= $maxLines) break; - } - } - } - fclose($fp); - return $lines; -} - // Logic for signaling $action = $_REQUEST["action"] ?? ""; $room = room_id($_REQUEST["room"] ?? "secours"); @@ -98,76 +36,94 @@ $my_id = $_REQUEST["peer_id"] ?? ""; if ($action === "join") { $name = $_REQUEST["name"] ?? "User"; - $p_file = room_participants_file($room); - $ps = read_json_file($p_file); - - // Cleanup old participants (> 10s) - $stale_time = now_ms() - 10000; - foreach ($ps as $id => $p) { - if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]); - } $new_id = peer_id(); - $ps[$new_id] = [ - "id" => $new_id, - "user_id" => $current_user_id, - "name" => $name, - "avatar_url" => $user["avatar_url"] ?? "", - "last_seen" => now_ms() - ]; - write_json_file($p_file, $ps); - - // DB Integration for sidebar if ($current_user_id > 0) { try { - $stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, last_seen) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, last_seen = ?"); - $stmt->execute([$current_user_id, $room, now_ms(), $room, now_ms()]); - } catch (Exception $e) {} + $stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, peer_id, name, last_seen) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, peer_id = ?, name = ?, last_seen = ?"); + $stmt->execute([$current_user_id, $room, $new_id, $name, now_ms(), $room, $new_id, $name, now_ms()]); + } catch (Exception $e) { + json_out(["error" => "DB Error: " . $e->getMessage()], 500); + } + } else { + json_out(["error" => "Not logged in"], 401); } + // Fetch all participants in this room + $ps = []; + try { + $stmt = db()->prepare("SELECT vs.*, u.avatar_url FROM voice_sessions vs JOIN users u ON vs.user_id = u.id WHERE vs.channel_id = ? AND vs.last_seen > ?"); + $stmt->execute([$room, now_ms() - 7000]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as $row) { + $ps[$row["peer_id"]] = [ + "id" => $row["peer_id"], + "user_id" => (int)$row["user_id"], + "name" => $row["name"], + "avatar_url" => $row["avatar_url"], + "last_seen" => (int)$row["last_seen"] + ]; + } + } catch (Exception $e) {} + json_out(["success" => true, "peer_id" => $new_id, "participants" => $ps]); } if ($action === "poll") { if (!$my_id) json_out(["error" => "Missing peer_id"], 400); - $p_file = room_participants_file($room); - $ps = read_json_file($p_file); - if (isset($ps[$my_id])) { - $ps[$my_id]["last_seen"] = now_ms(); - } - - $stale_time = now_ms() - 10000; - foreach ($ps as $id => $p) { - if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]); - } - write_json_file($p_file, $ps); - - // Update DB last_seen + // Update last_seen if ($current_user_id > 0) { try { - $stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ?"); - $stmt->execute([now_ms(), $current_user_id]); + $stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ? AND peer_id = ?"); + $stmt->execute([now_ms(), $current_user_id, $my_id]); } catch (Exception $e) {} } - // Read signals - $log_file = room_log_file($room); - $signals = []; - if (file_exists($log_file)) { - $lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - $remaining = []; - $now = now_ms(); - foreach ($lines as $line) { - $sig = json_decode($line, true); - if ($sig && isset($sig["to"]) && $sig["to"] === $my_id) { - $signals[] = $sig; - } elseif ($sig && ($now - ($sig["time"] ?? 0) < 30000)) { - $remaining[] = $line; - } + // Fetch participants + $ps = []; + try { + $stmt = db()->prepare("SELECT vs.*, u.avatar_url FROM voice_sessions vs JOIN users u ON vs.user_id = u.id WHERE vs.channel_id = ? AND vs.last_seen > ?"); + $stmt->execute([$room, now_ms() - 7000]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as $row) { + $ps[$row["peer_id"]] = [ + "id" => $row["peer_id"], + "user_id" => (int)$row["user_id"], + "name" => $row["name"], + "avatar_url" => $row["avatar_url"], + "last_seen" => (int)$row["last_seen"] + ]; } - file_put_contents($log_file, implode("\n", $remaining) . (empty($remaining) ? "" : "\n"), LOCK_EX); - } + } catch (Exception $e) {} + + // Read signals + $signals = []; + try { + $stmt = db()->prepare("SELECT * FROM voice_signals WHERE to_peer_id = ? AND room_id = ?"); + $stmt->execute([$my_id, $room]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + $ids_to_delete = []; + foreach ($rows as $row) { + $signals[] = [ + "from" => $row["from_peer_id"], + "to" => $row["to_peer_id"], + "data" => json_decode($row["data"], true), + "time" => (int)$row["created_at"] + ]; + $ids_to_delete[] = $row["id"]; + } + if (!empty($ids_to_delete)) { + $placeholders = implode(',', array_fill(0, count($ids_to_delete), '?')); + db()->prepare("DELETE FROM voice_signals WHERE id IN ($placeholders)")->execute($ids_to_delete); + } + + // Occasional cleanup of old signals + if (rand(1, 20) === 1) { + $stale_sig = now_ms() - 60000; + db()->prepare("DELETE FROM voice_signals WHERE created_at < ?")->execute([$stale_sig]); + } + } catch (Exception $e) {} json_out(["success" => true, "participants" => $ps, "signals" => $signals]); } @@ -178,27 +134,16 @@ if ($action === "signal") { $data = $_REQUEST["data"] ?? ""; if (!$to || !$data) json_out(["error" => "Missing to/data"], 400); - $sig = [ - "from" => $my_id, - "to" => $to, - "data" => json_decode($data, true), - "time" => now_ms() - ]; - - file_put_contents(room_log_file($room), json_encode($sig) . "\n", FILE_APPEND | LOCK_EX); + try { + $stmt = db()->prepare("INSERT INTO voice_signals (room_id, from_peer_id, to_peer_id, data, created_at) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$room, $my_id, $to, $data, now_ms()]); + } catch (Exception $e) { + json_out(["error" => "Signal Error: " . $e->getMessage()], 500); + } json_out(["success" => true]); } if ($action === "list_all") { - // Periodic cleanup of the DB table (stale sessions > 15s) - if (rand(1, 10) === 1) { - try { - $stale_db_time = now_ms() - 15000; - $stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?"); - $stmt->execute([$stale_db_time]); - } catch (Exception $e) {} - } - try { $stmt = db()->prepare(" SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url @@ -206,7 +151,7 @@ if ($action === "list_all") { JOIN users u ON vs.user_id = u.id WHERE vs.last_seen > ? "); - $stale_db_time = now_ms() - 15000; + $stale_db_time = now_ms() - 30000; $stmt->execute([$stale_db_time]); $sessions = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -222,12 +167,11 @@ if ($action === "list_all") { if ($action === "leave") { if ($my_id) { - $p_file = room_participants_file($room); - $ps = read_json_file($p_file); - unset($ps[$my_id]); - write_json_file($p_file, $ps); - } - if ($current_user_id > 0) { + try { + $stmt = db()->prepare("DELETE FROM voice_sessions WHERE peer_id = ?"); + $stmt->execute([$my_id]); + } catch (Exception $e) {} + } elseif ($current_user_id > 0) { try { $stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?"); $stmt->execute([$current_user_id]); @@ -236,4 +180,4 @@ if ($action === "leave") { json_out(["success" => true]); } -json_out(["error" => "Unknown action"], 404); \ No newline at end of file +json_out(["error" => "Unknown action"], 404); diff --git a/assets/js/main.js b/assets/js/main.js index e26e253..3b0e7be 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,11 +1,6 @@ document.addEventListener('DOMContentLoaded', () => { - const fileUpload = document.getElementById('file-upload'); - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const messagesList = document.getElementById('messages-list'); - const typingIndicator = document.getElementById('typing-indicator'); - - function scrollToBottom(force = false) { + // Scroll to bottom helper + const messagesList = document.getElementById('messages-list'); if (!messagesList) return; // Smart scroll: only scroll if user is already at the bottom or if forced (e.g. sending a message) @@ -20,8 +15,9 @@ document.addEventListener('DOMContentLoaded', () => { // Backup for non-smooth support or rendering delays setTimeout(() => { - if (force || messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold + 200) { - messagesList.scrollTop = messagesList.scrollHeight; + const currentList = document.getElementById('messages-list'); + if (currentList && (force || currentList.scrollHeight - currentList.scrollTop <= currentList.clientHeight + threshold + 200)) { + currentList.scrollTop = currentList.scrollHeight; } }, 100); } @@ -444,10 +440,68 @@ document.addEventListener('DOMContentLoaded', () => { // Scroll to bottom scrollToBottom(true); - const currentChannel = window.activeChannelId || new URLSearchParams(window.location.search).get('channel_id') || 1; - const currentThread = new URLSearchParams(window.location.search).get('thread_id'); + let currentChannel = window.activeChannelId || new URLSearchParams(window.location.search).get('channel_id') || 1; + let currentThread = new URLSearchParams(window.location.search).get('thread_id'); let typingTimeout; + async function loadChannel(url, pushState = true) { + try { + const resp = await fetch(url); + const html = await resp.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + const newChat = doc.querySelector('.chat-container'); + const newMembers = doc.querySelector('.members-sidebar'); + const newChannels = doc.querySelector('.channels-list'); + + if (newChat) { + const chatContainer = document.querySelector('.chat-container'); + if (chatContainer) chatContainer.innerHTML = newChat.innerHTML; + } + if (newMembers) { + const membersSidebar = document.querySelector('.members-sidebar'); + if (membersSidebar) { + membersSidebar.innerHTML = newMembers.innerHTML; + if (newMembers.classList.contains('hidden')) membersSidebar.classList.add('hidden'); + else membersSidebar.classList.remove('hidden'); + } + } + if (newChannels) { + const channelsList = document.querySelector('.channels-list'); + if (channelsList) channelsList.innerHTML = newChannels.innerHTML; + } + + // Update state + const urlObj = new URL(url, window.location.origin); + const params = urlObj.searchParams; + currentChannel = params.get('channel_id') || 1; + currentThread = params.get('thread_id'); + window.activeChannelId = currentChannel; + + // Update Title + document.title = doc.title; + + // Re-init features + findLastMessageId(); + scrollToBottom(true); + + if (pushState) history.pushState(null, '', url); + + // Re-sync voice UI to ensure green highlight is correct + if (window.voiceHandler) { + window.voiceHandler.updateVoiceUI(); + } + } catch (e) { + console.error('Failed to load channel:', e); + window.location.href = url; // Fallback + } + } + + window.addEventListener('popstate', () => { + loadChannel(window.location.href, false); + }); + // Notification Permission if ("Notification" in window && Notification.permission === "default") { Notification.requestPermission(); @@ -563,139 +617,160 @@ document.addEventListener('DOMContentLoaded', () => { }, 1000); function showTyping(username) { + const typingIndicator = document.getElementById('typing-indicator'); if (!typingIndicator) return; typingIndicator.textContent = `${username} is typing...`; clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { - if (typingIndicator) typingIndicator.textContent = ''; + const currentIndicator = document.getElementById('typing-indicator'); + if (currentIndicator) currentIndicator.textContent = ''; }, 3000); } - chatInput?.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { + document.addEventListener('keydown', (e) => { + if (e.target.id === 'chat-input') { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + const form = document.getElementById('chat-form'); + if (form) form.dispatchEvent(new Event('submit', { cancelable: true })); + } + } + }); + + document.addEventListener('input', (e) => { + if (e.target.id === 'chat-input') { + const chatInput = e.target; + chatInput.style.height = 'auto'; + chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px'; + if (chatInput.scrollHeight > 200) { + chatInput.style.overflowY = 'auto'; + } else { + chatInput.style.overflowY = 'hidden'; + } + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'typing', + channel_id: currentChannel, + user_id: window.currentUserId, + username: window.currentUsername + })); + } + } + }); + + document.addEventListener('submit', (e) => { + if (e.target.id === 'chat-form') { e.preventDefault(); - chatForm?.dispatchEvent(new Event('submit', { cancelable: true })); - } - }); - - chatInput?.addEventListener('input', () => { - chatInput.style.height = 'auto'; - chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px'; - if (chatInput.scrollHeight > 200) { - chatInput.style.overflowY = 'auto'; - } else { - chatInput.style.overflowY = 'hidden'; - } - - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ - type: 'typing', - channel_id: currentChannel, - user_id: window.currentUserId, - username: window.currentUsername - })); - } - }); - - chatForm?.addEventListener('submit', (e) => { - e.preventDefault(); - const content = chatInput.value.trim(); - const file = fileUpload.files[0]; - if (!content && !file) return; - - chatInput.value = ''; - chatInput.style.height = '24px'; - chatInput.style.overflowY = 'hidden'; - const formData = new FormData(); - formData.append('content', content); - formData.append('channel_id', currentChannel); - if (currentThread) { - formData.append('thread_id', currentThread); - } - - const progressContainer = document.getElementById('upload-progress-container'); - const progressBar = document.getElementById('upload-progress-bar'); - const progressPercent = document.getElementById('upload-percentage'); - const progressFilename = document.getElementById('upload-filename'); - - if (file) { - formData.append('file', file); - fileUpload.value = ''; // Clear file input + const form = e.target; + const chatInput = form.querySelector('#chat-input'); + const fileUpload = form.querySelector('#file-upload'); - // Show progress bar - progressContainer.style.display = 'block'; - progressFilename.textContent = `Uploading: ${file.name}`; - progressBar.style.width = '0%'; - progressPercent.textContent = '0%'; - } + const content = chatInput.value.trim(); + const file = fileUpload.files[0]; + if (!content && !file) return; - const xhr = new XMLHttpRequest(); - xhr.open('POST', 'api_v1_messages.php', true); - - xhr.upload.onprogress = (ev) => { - if (ev.lengthComputable && file) { - const percent = Math.round((ev.loaded / ev.total) * 100); - progressBar.style.width = percent + '%'; - progressPercent.textContent = percent + '%'; + chatInput.value = ''; + chatInput.style.height = '24px'; + chatInput.style.overflowY = 'hidden'; + const formData = new FormData(); + formData.append('content', content); + formData.append('channel_id', currentChannel); + if (currentThread) { + formData.append('thread_id', currentThread); } - }; + + const progressContainer = document.getElementById('upload-progress-container'); + const progressBar = document.getElementById('upload-progress-bar'); + const progressPercent = document.getElementById('upload-percentage'); + const progressFilename = document.getElementById('upload-filename'); - xhr.onload = () => { - if (xhr.status === 200) { - const result = JSON.parse(xhr.responseText); - if (result.success) { - appendMessage(result.message); - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ - type: 'message', - data: JSON.stringify({ - ...result.message, - channel_id: currentChannel - }) - })); - } - } else { - alert(result.error || 'Failed to send message'); + if (file) { + formData.append('file', file); + fileUpload.value = ''; // Clear file input + + // Show progress bar + if (progressContainer) progressContainer.style.display = 'block'; + if (progressFilename) progressFilename.textContent = `Uploading: ${file.name}`; + if (progressBar) progressBar.style.width = '0%'; + if (progressPercent) progressPercent.textContent = '0%'; + } + + const xhr = new XMLHttpRequest(); + xhr.open('POST', 'api_v1_messages.php', true); + + xhr.upload.onprogress = (ev) => { + if (ev.lengthComputable && file) { + const percent = Math.round((ev.loaded / ev.total) * 100); + if (progressBar) progressBar.style.width = percent + '%'; + if (progressPercent) progressPercent.textContent = percent + '%'; } - } - progressContainer.style.display = 'none'; - }; + }; - xhr.onerror = () => { - console.error('XHR Error'); - progressContainer.style.display = 'none'; - alert('An error occurred during the upload.'); - }; + xhr.onload = () => { + if (xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + if (result.success) { + appendMessage(result.message); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'message', + data: JSON.stringify({ + ...result.message, + channel_id: currentChannel + }) + })); + } + } else { + alert(result.error || 'Failed to send message'); + } + } + if (progressContainer) progressContainer.style.display = 'none'; + }; - xhr.send(formData); + xhr.onerror = () => { + console.error('XHR Error'); + if (progressContainer) progressContainer.style.display = 'none'; + alert('An error occurred during the upload.'); + }; + + xhr.send(formData); + } }); // Handle Click Events document.addEventListener('click', (e) => { + const link = e.target.closest('a'); + if (link && link.href && link.href.startsWith(window.location.origin) && !link.dataset.bsToggle && !link.dataset.bsTarget && link.target !== '_blank') { + const url = new URL(link.href); + const targetServerId = url.searchParams.get('server_id'); + const targetChannelId = url.searchParams.get('channel_id'); + const targetThreadId = url.searchParams.get('thread_id'); + + // If it's a channel or thread within the same server, use AJAX + if (targetServerId === activeServerId && (targetChannelId || targetThreadId)) { + e.preventDefault(); + loadChannel(link.href); + return; + } + + // Also handle DM switching + if (targetServerId === 'dms' && activeServerId === 'dms' && targetChannelId) { + e.preventDefault(); + loadChannel(link.href); + return; + } + } + console.log('Global click at:', e.target); - // Voice Channel Click const voiceItem = e.target.closest('.voice-item'); if (voiceItem) { e.preventDefault(); - console.log('Voice item clicked, Channel ID:', voiceItem.dataset.channelId); const channelId = voiceItem.dataset.channelId; - if (voiceHandler) { - if (voiceHandler.currentChannelId == channelId) { - console.log('Already in this channel:', channelId); - return; - } else { - console.log('Joining voice channel:', channelId); - voiceHandler.join(channelId); - // Update active state in UI - document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active')); - voiceItem.classList.add('active'); - } - } else { - console.error('voiceHandler not initialized'); - } + if (window.voiceHandler) window.voiceHandler.join(channelId); return; } - + const badge = e.target.closest('.reaction-badge'); if (badge) { const msgId = badge.parentElement.dataset.messageId; @@ -993,57 +1068,61 @@ document.addEventListener('DOMContentLoaded', () => { }); // Global Search - const searchInput = document.getElementById('global-search'); - const searchType = document.getElementById('search-type'); - const searchResults = document.getElementById('search-results'); - - searchInput?.addEventListener('input', async () => { - const q = searchInput.value.trim(); - const type = searchType.value; - if (q.length < 2) { - searchResults.style.display = 'none'; - return; - } - - const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&channel_id=${currentChannel}`); - const data = await resp.json(); - - if (data.success && data.results.length > 0) { - searchResults.innerHTML = ''; - data.results.forEach(res => { - const item = document.createElement('div'); - item.className = 'search-result-item d-flex align-items-center gap-2'; - if (type === 'users') { - item.innerHTML = ` -
-
-
${res.username}
-
Click to start conversation
-
- `; - item.onclick = () => { - const formData = new FormData(); - formData.append('user_id', res.id); - fetch('api_v1_dms.php', { method: 'POST', body: formData }) - .then(r => r.json()) - .then(resDM => { - if (resDM.success) window.location.href = `?server_id=dms&channel_id=${resDM.channel_id}`; - }); - }; + document.addEventListener('input', async (e) => { + if (e.target.id === 'global-search') { + const searchInput = e.target; + const searchType = document.getElementById('search-type'); + const searchResults = document.getElementById('search-results'); + + const q = searchInput.value.trim(); + const type = searchType ? searchType.value : 'messages'; + if (q.length < 2) { + if (searchResults) searchResults.style.display = 'none'; + return; + } + + const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&channel_id=${currentChannel}`); + const data = await resp.json(); + + if (searchResults) { + if (data.success && data.results.length > 0) { + searchResults.innerHTML = ''; + data.results.forEach(res => { + const item = document.createElement('div'); + item.className = 'search-result-item d-flex align-items-center gap-2'; + if (type === 'users') { + item.innerHTML = ` +
+
+
${res.username}
+
Click to start conversation
+
+ `; + item.onclick = () => { + const formData = new FormData(); + formData.append('user_id', res.id); + fetch('api_v1_dms.php', { method: 'POST', body: formData }) + .then(r => r.json()) + .then(resDM => { + if (resDM.success) loadChannel(`?server_id=dms&channel_id=${resDM.channel_id}`); + }); + }; + } else { + item.innerHTML = ` +
+
${escapeHTML(res.username)}
+
${parseCustomEmotes(res.content)}
+
+ `; + } + searchResults.appendChild(item); + }); + searchResults.style.display = 'block'; } else { - item.innerHTML = ` -
-
${escapeHTML(res.username)}
-
${parseCustomEmotes(res.content)}
-
- `; + searchResults.innerHTML = '
No results found
'; + searchResults.style.display = 'block'; } - searchResults.appendChild(item); - }); - searchResults.style.display = 'block'; - } else { - searchResults.innerHTML = '
No results found
'; - searchResults.style.display = 'block'; + } } }); @@ -2427,17 +2506,19 @@ document.addEventListener('DOMContentLoaded', () => { }); // Toggle members sidebar - const toggleMembersBtn = document.getElementById('toggle-members-btn'); - const membersSidebar = document.querySelector('.members-sidebar'); - if (toggleMembersBtn && membersSidebar) { - toggleMembersBtn.addEventListener('click', () => { - if (window.innerWidth > 992) { - membersSidebar.classList.toggle('hidden'); - } else { - membersSidebar.classList.toggle('show'); + document.addEventListener('click', (e) => { + const toggleMembersBtn = e.target.closest('#toggle-members-btn'); + if (toggleMembersBtn) { + const membersSidebar = document.querySelector('.members-sidebar'); + if (membersSidebar) { + if (window.innerWidth > 992) { + membersSidebar.classList.toggle('hidden'); + } else { + membersSidebar.classList.toggle('show'); + } } - }); - } + } + }); // User Settings - Save handled in index.php diff --git a/assets/js/voice.js b/assets/js/voice.js index 7232825..8390424 100644 --- a/assets/js/voice.js +++ b/assets/js/voice.js @@ -29,7 +29,7 @@ class VoiceChannel { this.speakingUsers = new Set(); this.setupPTTListeners(); - window.addEventListener('beforeunload', () => this.leave()); + window.addEventListener('beforeunload', () => this.leave(true)); } setupPTTListeners() { @@ -79,6 +79,7 @@ class VoiceChannel { } this.currentChannelId = channelId; + this.updateVoiceUI(); try { console.log('Requesting microphone access...'); @@ -102,9 +103,10 @@ class VoiceChannel { // Start polling this.startPolling(); - this.updateVoiceUI(); } else { console.error('API join failed:', data.error); + this.currentChannelId = null; + this.updateVoiceUI(); } } catch (e) { console.error('Failed to join voice:', e); @@ -380,12 +382,13 @@ class VoiceChannel { } } - leave() { + leave(isUnloading = false) { if (!this.currentChannelId) return; console.log('Leaving voice channel:', this.currentChannelId); if (this.pollInterval) clearInterval(this.pollInterval); - fetch(`api_v1_voice.php?action=leave&room=${this.currentChannelId}&peer_id=${this.myPeerId}`); + const url = `api_v1_voice.php?action=leave&room=${this.currentChannelId}&peer_id=${this.myPeerId}`; + fetch(url); if (this.localStream) { this.localStream.getTracks().forEach(track => track.stop()); @@ -425,12 +428,23 @@ class VoiceChannel { this.myPeerId = null; this.speakingUsers.clear(); this.updateVoiceUI(); + + return leavePromise; } updateVoiceUI() { // We now use a global update mechanism for all channels VoiceChannel.refreshAllVoiceUsers(); + // Update active class on channel items to reflect current voice connection + document.querySelectorAll('.voice-item').forEach(item => { + if (this.currentChannelId && item.dataset.channelId == this.currentChannelId) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + if (this.currentChannelId) { if (!document.querySelector('.voice-controls')) { const controls = document.createElement('div'); @@ -479,6 +493,18 @@ class VoiceChannel { const resp = await fetch('api_v1_voice.php?action=list_all'); const data = await resp.json(); if (data.success) { + // Update active class for local user to ensure UI is in sync + if (window.voiceHandler) { + const currentChannelId = window.voiceHandler.currentChannelId; + document.querySelectorAll('.voice-item').forEach(item => { + if (currentChannelId && item.dataset.channelId == currentChannelId) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + } + // Clear all lists first document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = ''); diff --git a/assets/pasted-20260218-144808-f4591d31.png b/assets/pasted-20260218-144808-f4591d31.png new file mode 100644 index 0000000000000000000000000000000000000000..6f5ab4a31d1874f0eed4b77db04d96626a956028 GIT binary patch literal 8843 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 AVgLXD literal 0 HcmV?d00001 diff --git a/assets/pasted-20260218-153932-fac8d7bf.png b/assets/pasted-20260218-153932-fac8d7bf.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5d979bfcde25a995be4e6b95d18158ab1d3050 GIT binary patch literal 39235 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

{HHgqMw^8eo81~~DQt@(z|h;C60O-NeK z)59|1y6}M~$c)X&0_e*49)SlcU~1cwYPp3k5m(i5|lgnEq0%Dor|@XMG{a$^La+Y9{6E{Fc+j++j2Q?cRwI@cJW zuK4YsW=s8Tpy^^316t{^;%VHaW0GrdiE7aj`Vmr`x@zbu6t+elNL#*hP8Cg|XK=dp zg3oT=<)uzWo3+W!2isBI$GSJ1FO*`;2Yyx;)+9P+G|+J6Fs>Zb?Z*}L=niFnjw3OS z!tdo3;Qga&kZqR=K$8*NRB2Lk(`Uq2Z4#)Ts&v;20kV&pu92dM};; znoul`(dx>!)6r+{zOe!q4<LzY!B6B+$tHx=4#a8 za$`kmXehDb$Fq+-RFRZRy&csDe9$-55c^EN519;r2apC-8qugyu+C-*LQ6=2mtb`! zG}3!l&C~rr%eNXX)-mqc)*xb+6a|&S&2O!ysfx+SeDUs2o87#V_llJv^#!Fw^3%m> z_K$+jIHnz2V7}pcg6RU^SP$ zvfFfFOZe_S#2W?SP31>boK ziO=_sp4Xcx^(2>TIVClYDwK1aF;45(81=ie688*5^CFdziZtNI3_D(Zrn8L>jSJ&< zv&BLkK9FN>>5U}qKX17sA{N?{xo#xSapU*E1B#Q-L>1WVKAs4oegvKXZu|ZFhs`M>{{8a@z!B@ z+9p6Qc1au{57XDw5_5F!)WSFYx8RlS@vBTC$AW=JP8e(l1Gj$buW#~zeOGln+@H>} z40N9T)s_c@LYAC>PQ2KFQQ+<=h&$Q48}GM&w7vl3kb(6$RR-U`B31wUBUS%urTp*E jDEhyUdsp~$eS(EAV#`1eXy^=0`B)A#=u>ioxE literal 0 HcmV?d00001 diff --git a/data/22.log b/data/22.log deleted file mode 100644 index e69de29..0000000 diff --git a/data/22.participants.json b/data/22.participants.json deleted file mode 100644 index 0637a08..0000000 --- a/data/22.participants.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/data/3.log b/data/3.log deleted file mode 100644 index 0750eb9..0000000 --- a/data/3.log +++ /dev/null @@ -1,4 +0,0 @@ -{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":true},"time":1771423958367} -{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":false},"time":1771423959219} -{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":true},"time":1771423959434} -{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":false},"time":1771423967708} diff --git a/data/3.participants.json b/data/3.participants.json deleted file mode 100644 index dd21f21..0000000 --- a/data/3.participants.json +++ /dev/null @@ -1 +0,0 @@ -{"d52b05b241a00981":{"id":"d52b05b241a00981","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771423987533},"c96c42356d6a3ee4":{"id":"c96c42356d6a3ee4","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771423988087}} \ No newline at end of file diff --git a/data/6.log b/data/6.log deleted file mode 100644 index e9bcc14..0000000 --- a/data/6.log +++ /dev/null @@ -1,10 +0,0 @@ -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"offer","offer":{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 1705174900585877835 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 AA:37:B9:FA:14:15:DC:34:29:97:DC:55:77:28:8E:74:C0:94:15:08:DF:5B:E9:CC:36:81:E5:D9:5C:49:FB:46\r\na=group:BUNDLE 0\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2\/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap-allow-mixed\r\na=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\na=fmtp:101 0-15\r\na=ice-pwd:075627a25c9f07d3908305504803057d\r\na=ice-ufrag:03d0add1\r\na=mid:0\r\na=msid:{7a2c5afc-d053-43fd-9196-3caf4510feb3} {8439b857-ead8-4cb1-8111-cb3400348461}\r\na=rtcp-mux\r\na=rtpmap:109 opus\/48000\/2\r\na=rtpmap:9 G722\/8000\/1\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:101 telephone-event\/8000\r\na=setup:actpass\r\na=ssrc:1663488241 cname:{7dcb8038-2b50-4e42-827c-c4617a225c01}\r\n"}},"time":1771336456644} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 1 UDP 2122252543 192.168.26.26 62572 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456645} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 1 TCP 2105524479 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456647} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 2 TCP 2105524478 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456652} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 2 UDP 2122252542 192.168.26.26 62573 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456654} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052351 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456720} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052350 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456757} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052863 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456776} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052862 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456802} -{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456805} diff --git a/data/6.participants.json b/data/6.participants.json deleted file mode 100644 index 2879778..0000000 --- a/data/6.participants.json +++ /dev/null @@ -1 +0,0 @@ -{"0cce3619c0f299fa":{"id":"0cce3619c0f299fa","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336452806},"75b3938d276e81e1":{"id":"75b3938d276e81e1","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336462293}} \ No newline at end of file diff --git a/data/test.participants.json b/data/test.participants.json deleted file mode 100644 index f1d0f40..0000000 --- a/data/test.participants.json +++ /dev/null @@ -1 +0,0 @@ -{"0fbf720bc2f110c0":{"id":"0fbf720bc2f110c0","name":"AI","last_seen":1771336229774}} \ No newline at end of file diff --git a/db/migrations/20260218_voice_db_signaling.sql b/db/migrations/20260218_voice_db_signaling.sql new file mode 100644 index 0000000..a26365d --- /dev/null +++ b/db/migrations/20260218_voice_db_signaling.sql @@ -0,0 +1,17 @@ +-- Migration to move voice signaling and participants to the database +ALTER TABLE voice_sessions ADD COLUMN peer_id VARCHAR(16) NOT NULL; +ALTER TABLE voice_sessions ADD COLUMN name VARCHAR(50); +-- Reset voice_sessions as we changed schema +TRUNCATE TABLE voice_sessions; + +CREATE TABLE IF NOT EXISTS voice_signals ( + id INT AUTO_INCREMENT PRIMARY KEY, + room_id VARCHAR(50) NOT NULL, + from_peer_id VARCHAR(16) NOT NULL, + to_peer_id VARCHAR(16) NOT NULL, + data TEXT NOT NULL, + created_at BIGINT NOT NULL, + INDEX (to_peer_id), + INDEX (room_id), + INDEX (created_at) +); diff --git a/index.php b/index.php index b0b8f2f..6255084 100644 --- a/index.php +++ b/index.php @@ -329,8 +329,9 @@ if ($is_dm_view) { SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url FROM voice_sessions vs JOIN users u ON vs.user_id = u.id + WHERE vs.last_seen > ? "); - $stmt_vs->execute(); + $stmt_vs->execute([ (int)floor(microtime(true) * 1000) - 60000 ]); // 60s grace period $voice_sessions = $stmt_vs->fetchAll(); $voice_users_by_channel = []; foreach($voice_sessions as $vs) { diff --git a/requests.log b/requests.log index 5c85d2d..6c22ec0 100644 --- a/requests.log +++ b/requests.log @@ -764,3 +764,52 @@ 2026-02-18 13:54:26 - GET /index.php?server_id=1 - POST: [] 2026-02-18 13:54:43 - GET /index.php?server_id=1 - POST: [] 2026-02-18 14:13:01 - GET /?fl_project=38443 - POST: [] +2026-02-18 14:14:21 - GET /?fl_project=38443 - POST: [] +2026-02-18 14:18:52 - GET /?fl_project=38443 - POST: [] +2026-02-18 14:31:53 - GET / - POST: [] +2026-02-18 14:31:56 - HEAD / - POST: [] +2026-02-18 14:32:15 - GET /?fl_project=38443 - POST: [] +2026-02-18 14:45:08 - GET /index.php?server_id=1 - POST: [] +2026-02-18 14:45:20 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:45:32 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-18 14:45:42 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:46:15 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:46:49 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:46:51 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:49:47 - GET /?fl_project=38443 - POST: [] +2026-02-18 14:51:06 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:51:16 - GET /index.php - POST: [] +2026-02-18 14:51:45 - GET /index.php - POST: [] +2026-02-18 14:51:46 - GET /index.php - POST: [] +2026-02-18 14:52:18 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-18 14:52:43 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 14:57:03 - GET /?fl_project=38443 - POST: [] +2026-02-18 15:28:39 - GET / - POST: [] +2026-02-18 15:28:47 - GET /?fl_project=38443 - POST: [] +2026-02-18 15:28:56 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 15:29:22 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 15:29:27 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 15:29:34 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:29:38 - GET /index.php?server_id=1&channel_id=22 - POST: [] +2026-02-18 15:29:41 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:30:03 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-18 15:36:16 - GET / - POST: [] +2026-02-18 15:36:35 - GET /?fl_project=38443 - POST: [] +2026-02-18 15:38:12 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-18 15:38:19 - GET /index.php - POST: [] +2026-02-18 15:44:09 - GET /?fl_project=38443 - POST: [] +2026-02-18 15:45:23 - GET / - POST: [] +2026-02-18 15:46:47 - GET /?fl_project=38443 - POST: [] +2026-02-18 15:48:17 - GET /index.php - POST: [] +2026-02-18 15:48:20 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:48:23 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-18 15:48:26 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:48:37 - GET /index.php - POST: [] +2026-02-18 15:48:39 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:48:54 - GET /index.php?server_id=1&channel_id=22 - POST: [] +2026-02-18 15:48:55 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:50:35 - GET / - POST: [] +2026-02-18 15:51:01 - GET /?fl_project=38443 - POST: [] +2026-02-18 15:54:51 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:54:55 - GET /index.php?server_id=1&channel_id=3 - POST: [] +2026-02-18 15:55:21 - GET /?fl_project=38443 - POST: []