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 0000000..6f5ab4a Binary files /dev/null and b/assets/pasted-20260218-144808-f4591d31.png differ diff --git a/assets/pasted-20260218-153932-fac8d7bf.png b/assets/pasted-20260218-153932-fac8d7bf.png new file mode 100644 index 0000000..8d5d979 Binary files /dev/null and b/assets/pasted-20260218-153932-fac8d7bf.png differ 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: []