Vocal canaux avec membres en apercu

This commit is contained in:
Flatlogic Bot 2026-02-18 19:09:18 +00:00
parent 4eaf7679f1
commit e6a755b1d6
7 changed files with 132 additions and 21 deletions

View File

@ -107,7 +107,7 @@ if ($action === "join") {
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
}
$new_id = peer_id();
$new_id = substr($_REQUEST["peer_id"] ?: peer_id(), 0, 16);
$ps[$new_id] = [
"id" => $new_id,
"user_id" => $current_user_id,
@ -120,9 +120,14 @@ if ($action === "join") {
// 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, last_seen, peer_id, name) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, last_seen = ?, peer_id = ?, name = ?");
$res = $stmt->execute([$current_user_id, $room, now_ms(), $new_id, $name, $room, now_ms(), $new_id, $name]);
if (!$res) {
error_log("Failed to insert voice session for user $current_user_id in room $room");
}
} catch (Exception $e) {
error_log("Voice session DB error: " . $e->getMessage());
}
}
json_out(["success" => true, "peer_id" => $new_id, "participants" => $ps]);

View File

@ -925,7 +925,7 @@ body {
}
/* Voice active state */
.voice-item.active {
.voice-item.active, .voice-item.connected {
background-color: rgba(35, 165, 89, 0.1);
color: #23a559 !important;
}

View File

@ -29,7 +29,22 @@ class VoiceChannel {
this.speakingUsers = new Set();
this.setupPTTListeners();
window.addEventListener('beforeunload', () => this.leave());
window.addEventListener('beforeunload', () => {
// We don't want to leave on page refresh if we want persistence
// but we might want to tell the server we are "still here" soon.
// Actually, for a simple refresh, we just let the session timeout or re-join.
});
// Auto-rejoin if we were in a channel
setTimeout(() => {
const savedChannelId = sessionStorage.getItem('activeVoiceChannel');
const savedPeerId = sessionStorage.getItem('activeVoicePeerId');
if (savedChannelId) {
console.log('Auto-rejoining voice channel:', savedChannelId);
if (savedPeerId) this.myPeerId = savedPeerId;
this.join(savedChannelId, true); // Pass true to indicate auto-rejoin
}
}, 200);
}
setupPTTListeners() {
@ -67,18 +82,19 @@ class VoiceChannel {
});
}
async join(channelId) {
console.log('VoiceChannel.join process started for channel:', channelId);
if (this.currentChannelId === channelId) {
async join(channelId, isAutoRejoin = false) {
console.log('VoiceChannel.join process started for channel:', channelId, 'isAutoRejoin:', isAutoRejoin);
if (this.currentChannelId === channelId && !isAutoRejoin) {
console.log('Already in this channel');
return;
}
if (this.currentChannelId) {
if (this.currentChannelId && this.currentChannelId != channelId) {
console.log('Leaving previous channel:', this.currentChannelId);
this.leave();
}
this.currentChannelId = channelId;
sessionStorage.setItem('activeVoiceChannel', channelId);
try {
console.log('Requesting microphone access...');
@ -91,13 +107,14 @@ class VoiceChannel {
// Join via PHP
console.log('Calling API join...');
const url = `api_v1_voice.php?action=join&room=${channelId}&name=${encodeURIComponent(window.currentUsername || 'Unknown')}`;
const url = `api_v1_voice.php?action=join&room=${channelId}&name=${encodeURIComponent(window.currentUsername || 'Unknown')}${this.myPeerId ? '&peer_id='+this.myPeerId : ''}`;
const resp = await fetch(url);
const data = await resp.json();
console.log('API join response:', data);
if (data.success) {
this.myPeerId = data.peer_id;
sessionStorage.setItem('activeVoicePeerId', this.myPeerId);
console.log('Joined room with peer_id:', this.myPeerId);
// Start polling
@ -381,14 +398,27 @@ class VoiceChannel {
}
leave() {
if (!this.currentChannelId) return;
console.log('Leaving voice channel:', this.currentChannelId);
if (!this.currentChannelId) {
console.log('VoiceChannel.leave called but no active channel');
return;
}
console.log('Leaving voice channel:', this.currentChannelId, 'myPeerId:', this.myPeerId);
const cid = this.currentChannelId;
const pid = this.myPeerId;
sessionStorage.removeItem('activeVoiceChannel');
sessionStorage.removeItem('activeVoicePeerId');
if (this.pollInterval) clearInterval(this.pollInterval);
fetch(`api_v1_voice.php?action=leave&room=${this.currentChannelId}&peer_id=${this.myPeerId}`);
// Use keepalive for the leave fetch to ensure it reaches the server during page unload
fetch(`api_v1_voice.php?action=leave&room=${cid}&peer_id=${pid}`, { keepalive: true });
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
console.log('Stopping local stream tracks');
this.localStream.getTracks().forEach(track => {
track.stop();
console.log('Track stopped:', track.kind);
});
this.localStream = null;
}
if (this.analysisStream) {
@ -482,11 +512,18 @@ class VoiceChannel {
// Clear all lists first
document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = '');
// Remove connected highlight from all voice items
document.querySelectorAll('.voice-item').forEach(el => {
el.classList.remove('connected');
});
// Populate based on data
Object.keys(data.channels).forEach(channelId => {
// Fix: The voice-users-list is a sibling of the container of the voice-item
const voiceItem = document.querySelector(`.voice-item[data-channel-id="${channelId}"]`);
if (voiceItem) {
// Highlight channel as connected if anyone is in it
voiceItem.classList.add('connected');
const container = voiceItem.closest('.channel-item-container');
if (container) {
const listEl = container.querySelector('.voice-users-list');

View File

@ -1,5 +1,52 @@
<?php
session_start();
require_once __DIR__ . '/session.php';
$user = getCurrentUser();
if ($user) {
try {
// Find which channel and peer they were in to clean up files too
$stmt = db()->prepare("SELECT channel_id, peer_id FROM voice_sessions WHERE user_id = ?");
$stmt->execute([$user['id']]);
$sess = $stmt->fetch();
if ($sess) {
$room = $sess['channel_id'];
$peerId = $sess['peer_id'];
// Clean up file-based participants
$p_file = __DIR__ . "/../data/" . $room . ".participants.json";
if (file_exists($p_file)) {
$raw = @file_get_contents($p_file);
if ($raw) {
$ps = json_decode($raw, true);
if (is_array($ps)) {
$found = false;
if (isset($ps[$peerId])) {
unset($ps[$peerId]);
$found = true;
}
// Also cleanup by user_id just in case
foreach ($ps as $id => $p) {
if (($p['user_id'] ?? 0) == $user['id']) {
unset($ps[$id]);
$found = true;
}
}
if ($found) {
file_put_contents($p_file, json_encode($ps), LOCK_EX);
}
}
}
}
}
// Clean up DB session
db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?")->execute([$user['id']]);
} catch (Exception $e) {
// Ignore errors during logout cleanup
}
}
session_destroy();
header('Location: login.php');
exit;

View File

@ -1 +1 @@
{"c936fedf9810ea51":{"id":"c936fedf9810ea51","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771433918233},"abad0e2fe8760303":{"id":"abad0e2fe8760303","user_id":0,"name":"swefheim","avatar_url":"","last_seen":1771433918373}}
{"3356a3073b77f72d":{"id":"3356a3073b77f72d","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771441757997},"d58aa0268cc9e8d0":{"id":"d58aa0268cc9e8d0","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771441758062}}

View File

@ -316,8 +316,10 @@ 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();
$stale_db_time = (int) floor(microtime(true) * 1000) - 15000;
$stmt_vs->execute([$stale_db_time]);
$voice_sessions = $stmt_vs->fetchAll();
$voice_users_by_channel = [];
foreach($voice_sessions as $vs) {
@ -553,7 +555,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php foreach($voice_users_by_channel[$c['id']] as $v_user): ?>
<div class="voice-user small text-muted d-flex align-items-center mb-1" data-user-id="<?php echo $v_user['user_id']; ?>">
<div class="message-avatar me-2" style="width: 16px; height: 16px; <?php echo $v_user['avatar_url'] ? "background-image: url('{$v_user['avatar_url']}');" : ""; ?>"></div>
<span><?php echo htmlspecialchars($v_user['display_name'] ?? $v_user['username']); ?></span>
<span style="font-size: 13px;"><?php echo htmlspecialchars($v_user['display_name'] ?? $v_user['username']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
@ -634,7 +636,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<a href="#" title="Settings" style="color: var(--text-muted); margin-right: 8px;" data-bs-toggle="modal" data-bs-target="#userSettingsModal">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</a>
<a href="auth/logout.php" title="Logout" style="color: var(--text-muted);"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg></a>
<a href="auth/logout.php" onclick="if(window.voiceHandler) window.voiceHandler.leave(); sessionStorage.clear();" title="Logout" style="color: var(--text-muted);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg></a>
</div>
</div>
<div style="padding: 10px; font-size: 10px; color: #4e5058; border-top: 1px solid #1e1f22;">

View File

@ -657,3 +657,22 @@
2026-02-18 16:56:38 - GET /index.php - POST: []
2026-02-18 16:57:15 - GET /?fl_project=38443 - POST: []
2026-02-18 16:57:23 - GET /index.php - POST: []
2026-02-18 17:00:18 - GET /?fl_project=38443 - POST: []
2026-02-18 18:08:55 - GET /index.php - POST: []
2026-02-18 18:09:11 - GET /index.php - POST: []
2026-02-18 18:09:30 - GET /index.php - POST: []
2026-02-18 18:09:31 - GET /index.php - POST: []
2026-02-18 18:10:17 - GET /index.php - POST: []
2026-02-18 18:16:02 - GET /?fl_project=38443 - POST: []
2026-02-18 18:24:35 - GET /index.php - POST: []
2026-02-18 18:24:40 - GET /index.php - POST: []
2026-02-18 18:24:50 - GET /index.php - POST: []
2026-02-18 18:25:18 - GET /index.php - POST: []
2026-02-18 18:25:21 - GET /index.php - POST: []
2026-02-18 18:28:00 - GET /?fl_project=38443 - POST: []
2026-02-18 18:39:54 - GET /?fl_project=38443 - POST: []
2026-02-18 18:43:31 - GET / - POST: []
2026-02-18 18:44:03 - GET /?fl_project=38443 - POST: []
2026-02-18 19:06:59 - GET /index.php - POST: []
2026-02-18 19:07:22 - GET /index.php - POST: []
2026-02-18 19:08:00 - GET /index.php?server_id=1&channel_id=6 - POST: []