From 88dde78581c3215d5c081c3c859516ec525a1003 Mon Sep 17 00:00:00 2001 From: olcxja Date: Sun, 23 Aug 2026 15:36:30 +0200 Subject: [PATCH] fix group call popup & improve vc topbar design --- .../src/main/assets/public/blah/en-cat.json | 2 +- .../src/main/assets/public/blah/en-us.json | 2 +- .../src/main/assets/public/blah/pl-pl.json | 2 +- android/app/src/main/assets/public/style.css | 30 +++- android/app/src/main/assets/public/voice.js | 141 +++++++++++++++--- webroot/blah/en-cat.json | 2 +- webroot/blah/en-us.json | 2 +- webroot/blah/pl-pl.json | 2 +- webroot/style.css | 30 +++- webroot/voice.js | 141 +++++++++++++++--- 10 files changed, 292 insertions(+), 62 deletions(-) diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index 2834dbe0..db64989f 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -293,7 +293,7 @@ "owner.transferred": "Purrship transferred successfully", "title.voice.chat": "voice chat :3", "status.voice.connecting": "connecting... :3", - "status.voice.connected": "connected :3", + "status.voice.disconnected": "disconnected :3", "action.call.mute": "mute microphone", "action.call.camera": "camera", "action.call.screen": "share screen", diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index 5486df54..ee67f52d 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -302,7 +302,7 @@ "owner.transferred": "Ownership transferred successfully", "title.voice.chat": "Voice Chat", "status.voice.connecting": "Connecting...", - "status.voice.connected": "Connected", + "status.voice.disconnected": "Disconnected", "action.call.mute": "Mute Microphone", "action.call.camera": "Camera", "action.call.screen": "Share Screen", diff --git a/android/app/src/main/assets/public/blah/pl-pl.json b/android/app/src/main/assets/public/blah/pl-pl.json index aa9e1fb0..7d6f14ab 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -302,7 +302,7 @@ "owner.transferred": "Własność przekazana pomyślnie", "title.voice.chat": "Czat głosowy", "status.voice.connecting": "Łączenie...", - "status.voice.connected": "Połączono", + "status.voice.disconnected": "Rozłączono", "action.call.mute": "Wycisz mikrofon", "action.call.camera": "Kamera", "action.call.screen": "Udostępnij ekran", diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index b1f08f2d..5e495bcb 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -1340,6 +1340,15 @@ input:checked + .toggle-slider:before { align-items: center; gap: 0.5rem; color: var(--text-color); + min-width: 0; +} +.voice-title span { + font-size: 0.95rem; + font-weight: 700; + max-width: 16rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .voice-title svg { width: 1.1rem; @@ -1351,10 +1360,12 @@ input:checked + .toggle-slider:before { display: flex; align-items: center; gap: 0.4rem; - font-size: 0.8rem; - font-weight: 600; + flex-shrink: 0; + font-size: 0.75rem; + font-weight: 500; color: var(--darker-text-color); - opacity: 0.85; + opacity: 0.75; + font-variant-numeric: tabular-nums; } #voice-status .status-dot { width: 0.55rem; @@ -1364,14 +1375,17 @@ input:checked + .toggle-slider:before { background: var(--darker-text-color); } #voice-status.connecting .status-dot { + background: hsl(35, 90%, 55%); animation: voice-dot-pulse 1.2s ease-in-out infinite; } -#voice-status.connected { - opacity: 1; -} #voice-status.connected .status-dot { background: var(--big-green); - box-shadow: 0 0 0.35rem rgba(120, 200, 120, 0.45); +} +#voice-status.disconnected .status-dot { + background: var(--big-red); +} +.status-ping:empty { + display: none; } @keyframes voice-dot-pulse { 50% { opacity: 0.25; } @@ -1414,7 +1428,7 @@ input:checked + .toggle-slider:before { border-radius: 0.65rem; overflow: hidden; cursor: pointer; - border: var(--border-width) solid transparent; + border: calc(var(--border-width) * 2) solid rgba(85, 85, 85); box-shadow: 0 0.25rem 0.625rem rgba(0, 0, 0, 0.3); transition: border-color 0.15s ease, box-shadow 0.2s ease; } diff --git a/android/app/src/main/assets/public/voice.js b/android/app/src/main/assets/public/voice.js index fb1a8f03..b3a68a16 100644 --- a/android/app/src/main/assets/public/voice.js +++ b/android/app/src/main/assets/public/voice.js @@ -44,13 +44,48 @@ function getStoredVolume(type, clientId) { return v === undefined ? 100 : v; } +let voiceConnectedAt = null; +let voiceTimerInterval = null; +let voicePingMs = null; + +function formatVoiceDuration(ms) { + let totalSec = Math.floor(ms / 1000); + let h = Math.floor(totalSec / 3600); + let m = Math.floor((totalSec % 3600) / 60); + let s = totalSec % 60; + let mm = String(m).padStart(2, "0"), ss = String(s).padStart(2, "0"); + return h > 0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`; +} + +function updateVoicePing() { + let pingEl = document.getElementById("voice-ping"); + if (pingEl) pingEl.innerText = voicePingMs !== null ? `${voicePingMs} ms` : ""; +} + function setVoiceStatus(state) { let el = document.getElementById("voice-status"); if (!el) return; el.classList.toggle("connected", state === "connected"); - el.classList.toggle("connecting", state !== "connected"); + el.classList.toggle("connecting", state === "connecting"); + el.classList.toggle("disconnected", state === "disconnected"); + let textEl = el.querySelector(".voice-status-text"); - if (textEl) textEl.innerText = processBlah(state === "connected" ? "status.voice.connected" : "status.voice.connecting"); + if (state === "connected") { + if (voiceConnectedAt === null) voiceConnectedAt = Date.now(); + if (!voiceTimerInterval) { + voiceTimerInterval = setInterval(() => { + let tickEl = document.querySelector("#voice-status .voice-status-text"); + if (tickEl && voiceConnectedAt !== null) tickEl.innerText = formatVoiceDuration(Date.now() - voiceConnectedAt); + }, 1000); + } + if (textEl) textEl.innerText = formatVoiceDuration(Date.now() - voiceConnectedAt); + updateVoicePing(); + } else { + if (voiceTimerInterval) { clearInterval(voiceTimerInterval); voiceTimerInterval = null; } + voiceConnectedAt = null; + if (state === "disconnected") { voicePingMs = null; updateVoicePing(); } + if (textEl) textEl.innerText = processBlah(state === "disconnected" ? "status.voice.disconnected" : "status.voice.connecting"); + } } function setTileSpeaking(clientId, speaking) { @@ -208,6 +243,7 @@ async function startVoiceSession(serverUrl) { showVoiceUI(); updateVoiceUIUsers(); setVoiceStatus("connecting"); + updateVoiceChatTitle(); try { const wsUrl = new URL(serverUrl); @@ -234,8 +270,11 @@ async function startVoiceSession(serverUrl) { try { const baseUrl = currentVoiceWs.url.split('/ws')[0]; const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:'); + const t0 = performance.now(); const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`); const usersRaw = await res.json(); + voicePingMs = Math.round(performance.now() - t0); + updateVoicePing(); const users = usersRaw.map(u => String(u)); let changed = (users.length !== voiceActiveUsers.length); for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true; @@ -485,23 +524,46 @@ function handleCallUpdate(data) { } async function resolveCallerInfo(roomId) { - let name = window.dmUsernameCache ? window.dmUsernameCache[roomId] : null; - if (name && name.includes(':')) name = name.split(':')[0]; - if (!name && window.fetchAsync) { + if (window.fetchEncrypted) { try { - let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${roomId}`); - if (fetched && !fetched.startsWith("error:")) { - name = fetched.includes(':') ? fetched.split(':')[0] : fetched; - if (window.dmUsernameCache) window.dmUsernameCache[roomId] = name; + let infoPayload = await fetchEncrypted("group/info", roomId); + if (infoPayload && infoPayload.startsWith("{")) { + let info = JSON.parse(infoPayload); + if (info.string2) { + let pfpUrl = null; + if (window.getGroupAvatarUrl) { + try { pfpUrl = await getGroupAvatarUrl(roomId, info.string2); } catch (e) {} + } + return { name: info.string2, pfpUrl }; + } } } catch (e) {} } - let pfpUrl = window.dmPfpCache ? window.dmPfpCache[roomId] : null; - if (!pfpUrl && window.getAvatarUrl) { - try { pfpUrl = await window.getAvatarUrl(roomId, name || "User"); } catch (e) {} - if (pfpUrl && window.dmPfpCache) window.dmPfpCache[roomId] = pfpUrl; + + let targetId = String(roomId); + let parts = targetId.split('_'); + let myId = `${window.id};${window.host}`; + if (parts.length === 2 && (parts[0] === myId || parts[1] === myId)) { + targetId = parts[0] === myId ? parts[1] : parts[0]; } - return { name: name || processBlah("title.unknown"), pfpUrl }; + + let name = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null; + if (!name && window.fetchAsync) { + try { + let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${targetId}`); + if (fetched && !fetched.startsWith("error:")) { + name = fetched; + if (window.dmUsernameCache) window.dmUsernameCache[targetId] = fetched; + } + } catch (e) {} + } + + let pfpUrl = window.dmPfpCache ? window.dmPfpCache[targetId] : null; + if (!pfpUrl && window.getAvatarUrl) { + try { pfpUrl = await window.getAvatarUrl(targetId, name || "User"); } catch (e) {} + if (pfpUrl && window.dmPfpCache) window.dmPfpCache[targetId] = pfpUrl; + } + return { name: (name ? name.split(':')[0] : null) || processBlah("title.unknown"), pfpUrl }; } function showIncomingCallUI(roomId) { @@ -600,6 +662,39 @@ async function handleRoomSignal(data) { } } +async function resolveVoiceChatTitle() { + try { + if (currentVoiceCallType === 'groups') { + if (!currentVoiceCallId || !window.fetchEncrypted) return null; + let infoPayload = await fetchEncrypted("group/info", currentVoiceCallId); + if (infoPayload && infoPayload.startsWith("{")) { + return JSON.parse(infoPayload).string2 || null; + } + return null; + } + if (!currentVoiceCallId) return null; + let parts = String(currentVoiceCallId).split('_'); + let myId = `${window.id};${window.host}`; + let targetId = parts[0] === myId ? parts[1] : parts[0]; + if (!targetId) return null; + let name = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null; + if (!name && window.fetchAsync) { + name = await window.fetchAsync(`${window.url}/idtoname?id=${targetId}`); + if (name && !name.startsWith("error:") && window.dmUsernameCache) window.dmUsernameCache[targetId] = name; + } + if (!name || name.startsWith("error:")) return null; + return name.split(':')[0]; + } catch (e) { + return null; + } +} + +async function updateVoiceChatTitle() { + let name = await resolveVoiceChatTitle(); + let el = document.querySelector("#voice-header .voice-title span"); + if (el && name) el.innerText = name; +} + function showVoiceUI() { if (voiceUiContainer) return; @@ -614,6 +709,7 @@ function showVoiceUI() {
+ ${processBlah("status.voice.connecting")}
@@ -834,8 +930,8 @@ function updateVoiceUIUsers() { } }); } - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - if (!authorName && u === String(window.id)) authorName = window.username; + let resolvedName = authorName && authorName !== u ? authorName : null; + if (!resolvedName && u === String(window.id) && window.username) resolvedName = window.host ? `${window.username}:${window.host}` : window.username; let avatarRes = window.dmPfpCache ? window.dmPfpCache[u] : null; let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png"; @@ -852,7 +948,7 @@ function updateVoiceUIUsers() { div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`; let nameEl = document.getElementById(`voice-name-${u}`); - if (nameEl) nameEl.innerText = authorName || u; + if (nameEl) nameEl.innerText = resolvedName ? `${resolvedName} (${u})` : String(u); } Array.from(container.children).forEach(child => { @@ -890,10 +986,13 @@ function updateVoiceUIUsers() { } let authorName = window.dmUsernameCache ? window.dmUsernameCache[clientId] : null; - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - + if (!authorName && clientId === String(window.id) && window.username) authorName = window.host ? `${window.username}:${window.host}` : window.username; + + let streamLabel = processBlah(type == '1' ? "label.stream.cam" : "label.stream.screen"); + let baseName = authorName && authorName !== clientId ? `${authorName} (${clientId})` : clientId; + let nameEl = document.getElementById(`voice-stream-name-${decoderId}`); - if (nameEl) nameEl.innerText = `${authorName || clientId} (${processBlah(type == '1' ? "label.stream.cam" : "label.stream.screen")})`; + if (nameEl) nameEl.innerText = `${baseName} · ${streamLabel}`; } Array.from(container.children).forEach(child => { @@ -1048,6 +1147,8 @@ function toggleVoiceMic() { } function endVoiceSession() { + setVoiceStatus("disconnected"); + if (currentVoiceCallId) { let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId }); if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{}); diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index 2834dbe0..db64989f 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -293,7 +293,7 @@ "owner.transferred": "Purrship transferred successfully", "title.voice.chat": "voice chat :3", "status.voice.connecting": "connecting... :3", - "status.voice.connected": "connected :3", + "status.voice.disconnected": "disconnected :3", "action.call.mute": "mute microphone", "action.call.camera": "camera", "action.call.screen": "share screen", diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index 5486df54..ee67f52d 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -302,7 +302,7 @@ "owner.transferred": "Ownership transferred successfully", "title.voice.chat": "Voice Chat", "status.voice.connecting": "Connecting...", - "status.voice.connected": "Connected", + "status.voice.disconnected": "Disconnected", "action.call.mute": "Mute Microphone", "action.call.camera": "Camera", "action.call.screen": "Share Screen", diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json index aa9e1fb0..7d6f14ab 100644 --- a/webroot/blah/pl-pl.json +++ b/webroot/blah/pl-pl.json @@ -302,7 +302,7 @@ "owner.transferred": "Własność przekazana pomyślnie", "title.voice.chat": "Czat głosowy", "status.voice.connecting": "Łączenie...", - "status.voice.connected": "Połączono", + "status.voice.disconnected": "Rozłączono", "action.call.mute": "Wycisz mikrofon", "action.call.camera": "Kamera", "action.call.screen": "Udostępnij ekran", diff --git a/webroot/style.css b/webroot/style.css index b1f08f2d..5e495bcb 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -1340,6 +1340,15 @@ input:checked + .toggle-slider:before { align-items: center; gap: 0.5rem; color: var(--text-color); + min-width: 0; +} +.voice-title span { + font-size: 0.95rem; + font-weight: 700; + max-width: 16rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .voice-title svg { width: 1.1rem; @@ -1351,10 +1360,12 @@ input:checked + .toggle-slider:before { display: flex; align-items: center; gap: 0.4rem; - font-size: 0.8rem; - font-weight: 600; + flex-shrink: 0; + font-size: 0.75rem; + font-weight: 500; color: var(--darker-text-color); - opacity: 0.85; + opacity: 0.75; + font-variant-numeric: tabular-nums; } #voice-status .status-dot { width: 0.55rem; @@ -1364,14 +1375,17 @@ input:checked + .toggle-slider:before { background: var(--darker-text-color); } #voice-status.connecting .status-dot { + background: hsl(35, 90%, 55%); animation: voice-dot-pulse 1.2s ease-in-out infinite; } -#voice-status.connected { - opacity: 1; -} #voice-status.connected .status-dot { background: var(--big-green); - box-shadow: 0 0 0.35rem rgba(120, 200, 120, 0.45); +} +#voice-status.disconnected .status-dot { + background: var(--big-red); +} +.status-ping:empty { + display: none; } @keyframes voice-dot-pulse { 50% { opacity: 0.25; } @@ -1414,7 +1428,7 @@ input:checked + .toggle-slider:before { border-radius: 0.65rem; overflow: hidden; cursor: pointer; - border: var(--border-width) solid transparent; + border: calc(var(--border-width) * 2) solid rgba(85, 85, 85); box-shadow: 0 0.25rem 0.625rem rgba(0, 0, 0, 0.3); transition: border-color 0.15s ease, box-shadow 0.2s ease; } diff --git a/webroot/voice.js b/webroot/voice.js index fb1a8f03..b3a68a16 100644 --- a/webroot/voice.js +++ b/webroot/voice.js @@ -44,13 +44,48 @@ function getStoredVolume(type, clientId) { return v === undefined ? 100 : v; } +let voiceConnectedAt = null; +let voiceTimerInterval = null; +let voicePingMs = null; + +function formatVoiceDuration(ms) { + let totalSec = Math.floor(ms / 1000); + let h = Math.floor(totalSec / 3600); + let m = Math.floor((totalSec % 3600) / 60); + let s = totalSec % 60; + let mm = String(m).padStart(2, "0"), ss = String(s).padStart(2, "0"); + return h > 0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`; +} + +function updateVoicePing() { + let pingEl = document.getElementById("voice-ping"); + if (pingEl) pingEl.innerText = voicePingMs !== null ? `${voicePingMs} ms` : ""; +} + function setVoiceStatus(state) { let el = document.getElementById("voice-status"); if (!el) return; el.classList.toggle("connected", state === "connected"); - el.classList.toggle("connecting", state !== "connected"); + el.classList.toggle("connecting", state === "connecting"); + el.classList.toggle("disconnected", state === "disconnected"); + let textEl = el.querySelector(".voice-status-text"); - if (textEl) textEl.innerText = processBlah(state === "connected" ? "status.voice.connected" : "status.voice.connecting"); + if (state === "connected") { + if (voiceConnectedAt === null) voiceConnectedAt = Date.now(); + if (!voiceTimerInterval) { + voiceTimerInterval = setInterval(() => { + let tickEl = document.querySelector("#voice-status .voice-status-text"); + if (tickEl && voiceConnectedAt !== null) tickEl.innerText = formatVoiceDuration(Date.now() - voiceConnectedAt); + }, 1000); + } + if (textEl) textEl.innerText = formatVoiceDuration(Date.now() - voiceConnectedAt); + updateVoicePing(); + } else { + if (voiceTimerInterval) { clearInterval(voiceTimerInterval); voiceTimerInterval = null; } + voiceConnectedAt = null; + if (state === "disconnected") { voicePingMs = null; updateVoicePing(); } + if (textEl) textEl.innerText = processBlah(state === "disconnected" ? "status.voice.disconnected" : "status.voice.connecting"); + } } function setTileSpeaking(clientId, speaking) { @@ -208,6 +243,7 @@ async function startVoiceSession(serverUrl) { showVoiceUI(); updateVoiceUIUsers(); setVoiceStatus("connecting"); + updateVoiceChatTitle(); try { const wsUrl = new URL(serverUrl); @@ -234,8 +270,11 @@ async function startVoiceSession(serverUrl) { try { const baseUrl = currentVoiceWs.url.split('/ws')[0]; const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:'); + const t0 = performance.now(); const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`); const usersRaw = await res.json(); + voicePingMs = Math.round(performance.now() - t0); + updateVoicePing(); const users = usersRaw.map(u => String(u)); let changed = (users.length !== voiceActiveUsers.length); for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true; @@ -485,23 +524,46 @@ function handleCallUpdate(data) { } async function resolveCallerInfo(roomId) { - let name = window.dmUsernameCache ? window.dmUsernameCache[roomId] : null; - if (name && name.includes(':')) name = name.split(':')[0]; - if (!name && window.fetchAsync) { + if (window.fetchEncrypted) { try { - let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${roomId}`); - if (fetched && !fetched.startsWith("error:")) { - name = fetched.includes(':') ? fetched.split(':')[0] : fetched; - if (window.dmUsernameCache) window.dmUsernameCache[roomId] = name; + let infoPayload = await fetchEncrypted("group/info", roomId); + if (infoPayload && infoPayload.startsWith("{")) { + let info = JSON.parse(infoPayload); + if (info.string2) { + let pfpUrl = null; + if (window.getGroupAvatarUrl) { + try { pfpUrl = await getGroupAvatarUrl(roomId, info.string2); } catch (e) {} + } + return { name: info.string2, pfpUrl }; + } } } catch (e) {} } - let pfpUrl = window.dmPfpCache ? window.dmPfpCache[roomId] : null; - if (!pfpUrl && window.getAvatarUrl) { - try { pfpUrl = await window.getAvatarUrl(roomId, name || "User"); } catch (e) {} - if (pfpUrl && window.dmPfpCache) window.dmPfpCache[roomId] = pfpUrl; + + let targetId = String(roomId); + let parts = targetId.split('_'); + let myId = `${window.id};${window.host}`; + if (parts.length === 2 && (parts[0] === myId || parts[1] === myId)) { + targetId = parts[0] === myId ? parts[1] : parts[0]; } - return { name: name || processBlah("title.unknown"), pfpUrl }; + + let name = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null; + if (!name && window.fetchAsync) { + try { + let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${targetId}`); + if (fetched && !fetched.startsWith("error:")) { + name = fetched; + if (window.dmUsernameCache) window.dmUsernameCache[targetId] = fetched; + } + } catch (e) {} + } + + let pfpUrl = window.dmPfpCache ? window.dmPfpCache[targetId] : null; + if (!pfpUrl && window.getAvatarUrl) { + try { pfpUrl = await window.getAvatarUrl(targetId, name || "User"); } catch (e) {} + if (pfpUrl && window.dmPfpCache) window.dmPfpCache[targetId] = pfpUrl; + } + return { name: (name ? name.split(':')[0] : null) || processBlah("title.unknown"), pfpUrl }; } function showIncomingCallUI(roomId) { @@ -600,6 +662,39 @@ async function handleRoomSignal(data) { } } +async function resolveVoiceChatTitle() { + try { + if (currentVoiceCallType === 'groups') { + if (!currentVoiceCallId || !window.fetchEncrypted) return null; + let infoPayload = await fetchEncrypted("group/info", currentVoiceCallId); + if (infoPayload && infoPayload.startsWith("{")) { + return JSON.parse(infoPayload).string2 || null; + } + return null; + } + if (!currentVoiceCallId) return null; + let parts = String(currentVoiceCallId).split('_'); + let myId = `${window.id};${window.host}`; + let targetId = parts[0] === myId ? parts[1] : parts[0]; + if (!targetId) return null; + let name = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null; + if (!name && window.fetchAsync) { + name = await window.fetchAsync(`${window.url}/idtoname?id=${targetId}`); + if (name && !name.startsWith("error:") && window.dmUsernameCache) window.dmUsernameCache[targetId] = name; + } + if (!name || name.startsWith("error:")) return null; + return name.split(':')[0]; + } catch (e) { + return null; + } +} + +async function updateVoiceChatTitle() { + let name = await resolveVoiceChatTitle(); + let el = document.querySelector("#voice-header .voice-title span"); + if (el && name) el.innerText = name; +} + function showVoiceUI() { if (voiceUiContainer) return; @@ -614,6 +709,7 @@ function showVoiceUI() {
+ ${processBlah("status.voice.connecting")}
@@ -834,8 +930,8 @@ function updateVoiceUIUsers() { } }); } - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - if (!authorName && u === String(window.id)) authorName = window.username; + let resolvedName = authorName && authorName !== u ? authorName : null; + if (!resolvedName && u === String(window.id) && window.username) resolvedName = window.host ? `${window.username}:${window.host}` : window.username; let avatarRes = window.dmPfpCache ? window.dmPfpCache[u] : null; let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png"; @@ -852,7 +948,7 @@ function updateVoiceUIUsers() { div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`; let nameEl = document.getElementById(`voice-name-${u}`); - if (nameEl) nameEl.innerText = authorName || u; + if (nameEl) nameEl.innerText = resolvedName ? `${resolvedName} (${u})` : String(u); } Array.from(container.children).forEach(child => { @@ -890,10 +986,13 @@ function updateVoiceUIUsers() { } let authorName = window.dmUsernameCache ? window.dmUsernameCache[clientId] : null; - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - + if (!authorName && clientId === String(window.id) && window.username) authorName = window.host ? `${window.username}:${window.host}` : window.username; + + let streamLabel = processBlah(type == '1' ? "label.stream.cam" : "label.stream.screen"); + let baseName = authorName && authorName !== clientId ? `${authorName} (${clientId})` : clientId; + let nameEl = document.getElementById(`voice-stream-name-${decoderId}`); - if (nameEl) nameEl.innerText = `${authorName || clientId} (${processBlah(type == '1' ? "label.stream.cam" : "label.stream.screen")})`; + if (nameEl) nameEl.innerText = `${baseName} · ${streamLabel}`; } Array.from(container.children).forEach(child => { @@ -1048,6 +1147,8 @@ function toggleVoiceMic() { } function endVoiceSession() { + setVoiceStatus("disconnected"); + if (currentVoiceCallId) { let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId }); if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{});