Autosave: 20260218-155536

This commit is contained in:
Flatlogic Bot 2026-02-18 15:55:37 +00:00
parent 05c351eb39
commit 631deb62c1
15 changed files with 440 additions and 340 deletions

View File

@ -1,5 +1,4 @@
<?php // Vocal secours — WebRTC P2P + signalisation PHP + participants + PTT + VOX (gating via GainNode)
// Mutualisé OVH : nécessite ./data écrivable.
<?php // Vocal secours — WebRTC P2P + signalisation PHP via DB + participants via DB
declare(strict_types=1);
require_once "auth/session.php";
header("X-Content-Type-Options: nosniff");
@ -7,9 +6,6 @@ header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
$DATA_DIR = __DIR__ . "/data";
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
$user = getCurrentUser();
$current_user_id = $user ? (int)$user["id"] : 0;
@ -22,20 +18,6 @@ function peer_id(): string {
return bin2hex(random_bytes(8));
}
function room_log_file(string $room): string {
return __DIR__ . "/data/" . $room . ".log";
}
function room_participants_file(string $room): string {
return __DIR__ . "/data/" . $room . ".participants.json";
}
function chat_log_file_for_today(): string {
// Un fichier par jour : YYYY-MM-DD.chat.log
$d = date("Y-m-d");
return __DIR__ . "/data/" . $d . ".chat.log";
}
function now_ms(): int {
return (int) floor(microtime(true) * 1000);
}
@ -47,50 +29,6 @@ function json_out($data, int $code = 200): void {
exit;
}
function read_json_file(string $path): array {
if (!file_exists($path)) return [];
$raw = @file_get_contents($path);
if ($raw === false || $raw === "") return [];
$j = json_decode($raw, true);
return is_array($j) ? $j : [];
}
function write_json_file(string $path, array $data): void {
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
}
function tail_lines(string $path, int $maxLines = 120): array {
if (!is_file($path)) return [];
$fp = fopen($path, "rb");
if (!$fp) return [];
$lines = [];
fseek($fp, 0, SEEK_END);
$pos = ftell($fp);
$buffer = "";
while ($pos > 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);
json_out(["error" => "Unknown action"], 404);

View File

@ -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 = `
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
<div class="flex-grow-1">
<div class="search-result-author">${res.username}</div>
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
</div>
`;
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 = `
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
<div class="flex-grow-1">
<div class="search-result-author">${res.username}</div>
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
</div>
`;
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 = `
<div class="flex-grow-1">
<div class="search-result-author">${escapeHTML(res.username)}</div>
<div class="search-result-text">${parseCustomEmotes(res.content)}</div>
</div>
`;
}
searchResults.appendChild(item);
});
searchResults.style.display = 'block';
} else {
item.innerHTML = `
<div class="flex-grow-1">
<div class="search-result-author">${escapeHTML(res.username)}</div>
<div class="search-result-text">${parseCustomEmotes(res.content)}</div>
</div>
`;
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
searchResults.style.display = 'block';
}
searchResults.appendChild(item);
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
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

View File

@ -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 = '');

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

View File

@ -1 +0,0 @@
[]

View File

@ -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}

View File

@ -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}}

View File

@ -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}

View File

@ -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}}

View File

@ -1 +0,0 @@
{"0fbf720bc2f110c0":{"id":"0fbf720bc2f110c0","name":"AI","last_seen":1771336229774}}

View File

@ -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)
);

View File

@ -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) {

View File

@ -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: []