fix group call popup & improve vc topbar design
All checks were successful
Android Build / publish (push) Successful in 51s
Linux Build / publish (push) Successful in 1m0s

This commit is contained in:
olcxja 2026-08-23 15:36:30 +02:00
commit 88dde78581
10 changed files with 286 additions and 56 deletions

View file

@ -293,7 +293,7 @@
"owner.transferred": "Purrship transferred successfully", "owner.transferred": "Purrship transferred successfully",
"title.voice.chat": "voice chat :3", "title.voice.chat": "voice chat :3",
"status.voice.connecting": "connecting... :3", "status.voice.connecting": "connecting... :3",
"status.voice.connected": "connected :3", "status.voice.disconnected": "disconnected :3",
"action.call.mute": "mute microphone", "action.call.mute": "mute microphone",
"action.call.camera": "camera", "action.call.camera": "camera",
"action.call.screen": "share screen", "action.call.screen": "share screen",

View file

@ -302,7 +302,7 @@
"owner.transferred": "Ownership transferred successfully", "owner.transferred": "Ownership transferred successfully",
"title.voice.chat": "Voice Chat", "title.voice.chat": "Voice Chat",
"status.voice.connecting": "Connecting...", "status.voice.connecting": "Connecting...",
"status.voice.connected": "Connected", "status.voice.disconnected": "Disconnected",
"action.call.mute": "Mute Microphone", "action.call.mute": "Mute Microphone",
"action.call.camera": "Camera", "action.call.camera": "Camera",
"action.call.screen": "Share Screen", "action.call.screen": "Share Screen",

View file

@ -302,7 +302,7 @@
"owner.transferred": "Własność przekazana pomyślnie", "owner.transferred": "Własność przekazana pomyślnie",
"title.voice.chat": "Czat głosowy", "title.voice.chat": "Czat głosowy",
"status.voice.connecting": "Łączenie...", "status.voice.connecting": "Łączenie...",
"status.voice.connected": "Połączono", "status.voice.disconnected": "Rozłączono",
"action.call.mute": "Wycisz mikrofon", "action.call.mute": "Wycisz mikrofon",
"action.call.camera": "Kamera", "action.call.camera": "Kamera",
"action.call.screen": "Udostępnij ekran", "action.call.screen": "Udostępnij ekran",

View file

@ -1340,6 +1340,15 @@ input:checked + .toggle-slider:before {
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
color: var(--text-color); 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 { .voice-title svg {
width: 1.1rem; width: 1.1rem;
@ -1351,10 +1360,12 @@ input:checked + .toggle-slider:before {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
font-size: 0.8rem; flex-shrink: 0;
font-weight: 600; font-size: 0.75rem;
font-weight: 500;
color: var(--darker-text-color); color: var(--darker-text-color);
opacity: 0.85; opacity: 0.75;
font-variant-numeric: tabular-nums;
} }
#voice-status .status-dot { #voice-status .status-dot {
width: 0.55rem; width: 0.55rem;
@ -1364,14 +1375,17 @@ input:checked + .toggle-slider:before {
background: var(--darker-text-color); background: var(--darker-text-color);
} }
#voice-status.connecting .status-dot { #voice-status.connecting .status-dot {
background: hsl(35, 90%, 55%);
animation: voice-dot-pulse 1.2s ease-in-out infinite; animation: voice-dot-pulse 1.2s ease-in-out infinite;
} }
#voice-status.connected {
opacity: 1;
}
#voice-status.connected .status-dot { #voice-status.connected .status-dot {
background: var(--big-green); 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 { @keyframes voice-dot-pulse {
50% { opacity: 0.25; } 50% { opacity: 0.25; }
@ -1414,7 +1428,7 @@ input:checked + .toggle-slider:before {
border-radius: 0.65rem; border-radius: 0.65rem;
overflow: hidden; overflow: hidden;
cursor: pointer; 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); box-shadow: 0 0.25rem 0.625rem rgba(0, 0, 0, 0.3);
transition: border-color 0.15s ease, box-shadow 0.2s ease; transition: border-color 0.15s ease, box-shadow 0.2s ease;
} }

View file

@ -44,13 +44,48 @@ function getStoredVolume(type, clientId) {
return v === undefined ? 100 : v; 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) { function setVoiceStatus(state) {
let el = document.getElementById("voice-status"); let el = document.getElementById("voice-status");
if (!el) return; if (!el) return;
el.classList.toggle("connected", state === "connected"); 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"); 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) { function setTileSpeaking(clientId, speaking) {
@ -208,6 +243,7 @@ async function startVoiceSession(serverUrl) {
showVoiceUI(); showVoiceUI();
updateVoiceUIUsers(); updateVoiceUIUsers();
setVoiceStatus("connecting"); setVoiceStatus("connecting");
updateVoiceChatTitle();
try { try {
const wsUrl = new URL(serverUrl); const wsUrl = new URL(serverUrl);
@ -234,8 +270,11 @@ async function startVoiceSession(serverUrl) {
try { try {
const baseUrl = currentVoiceWs.url.split('/ws')[0]; const baseUrl = currentVoiceWs.url.split('/ws')[0];
const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:'); const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:');
const t0 = performance.now();
const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`); const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`);
const usersRaw = await res.json(); const usersRaw = await res.json();
voicePingMs = Math.round(performance.now() - t0);
updateVoicePing();
const users = usersRaw.map(u => String(u)); const users = usersRaw.map(u => String(u));
let changed = (users.length !== voiceActiveUsers.length); let changed = (users.length !== voiceActiveUsers.length);
for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true; for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true;
@ -485,23 +524,46 @@ function handleCallUpdate(data) {
} }
async function resolveCallerInfo(roomId) { async function resolveCallerInfo(roomId) {
let name = window.dmUsernameCache ? window.dmUsernameCache[roomId] : null; if (window.fetchEncrypted) {
if (name && name.includes(':')) name = name.split(':')[0];
if (!name && window.fetchAsync) {
try { try {
let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${roomId}`); let infoPayload = await fetchEncrypted("group/info", roomId);
if (fetched && !fetched.startsWith("error:")) { if (infoPayload && infoPayload.startsWith("{")) {
name = fetched.includes(':') ? fetched.split(':')[0] : fetched; let info = JSON.parse(infoPayload);
if (window.dmUsernameCache) window.dmUsernameCache[roomId] = name; 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) {} } catch (e) {}
} }
let pfpUrl = window.dmPfpCache ? window.dmPfpCache[roomId] : null;
if (!pfpUrl && window.getAvatarUrl) { let targetId = String(roomId);
try { pfpUrl = await window.getAvatarUrl(roomId, name || "User"); } catch (e) {} let parts = targetId.split('_');
if (pfpUrl && window.dmPfpCache) window.dmPfpCache[roomId] = pfpUrl; 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) { 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() { function showVoiceUI() {
if (voiceUiContainer) return; if (voiceUiContainer) return;
@ -614,6 +709,7 @@ function showVoiceUI() {
</div> </div>
<div id="voice-status" class="connecting"> <div id="voice-status" class="connecting">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="status-ping" id="voice-ping"></span>
<span class="voice-status-text">${processBlah("status.voice.connecting")}</span> <span class="voice-status-text">${processBlah("status.voice.connecting")}</span>
</div> </div>
</div> </div>
@ -834,8 +930,8 @@ function updateVoiceUIUsers() {
} }
}); });
} }
if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; let resolvedName = authorName && authorName !== u ? authorName : null;
if (!authorName && u === String(window.id)) authorName = window.username; 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 avatarRes = window.dmPfpCache ? window.dmPfpCache[u] : null;
let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png"; let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png";
@ -852,7 +948,7 @@ function updateVoiceUIUsers() {
div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`; div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`;
let nameEl = document.getElementById(`voice-name-${u}`); 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 => { Array.from(container.children).forEach(child => {
@ -890,10 +986,13 @@ function updateVoiceUIUsers() {
} }
let authorName = window.dmUsernameCache ? window.dmUsernameCache[clientId] : null; 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}`); 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 => { Array.from(container.children).forEach(child => {
@ -1048,6 +1147,8 @@ function toggleVoiceMic() {
} }
function endVoiceSession() { function endVoiceSession() {
setVoiceStatus("disconnected");
if (currentVoiceCallId) { if (currentVoiceCallId) {
let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId }); let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId });
if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{}); if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{});

View file

@ -293,7 +293,7 @@
"owner.transferred": "Purrship transferred successfully", "owner.transferred": "Purrship transferred successfully",
"title.voice.chat": "voice chat :3", "title.voice.chat": "voice chat :3",
"status.voice.connecting": "connecting... :3", "status.voice.connecting": "connecting... :3",
"status.voice.connected": "connected :3", "status.voice.disconnected": "disconnected :3",
"action.call.mute": "mute microphone", "action.call.mute": "mute microphone",
"action.call.camera": "camera", "action.call.camera": "camera",
"action.call.screen": "share screen", "action.call.screen": "share screen",

View file

@ -302,7 +302,7 @@
"owner.transferred": "Ownership transferred successfully", "owner.transferred": "Ownership transferred successfully",
"title.voice.chat": "Voice Chat", "title.voice.chat": "Voice Chat",
"status.voice.connecting": "Connecting...", "status.voice.connecting": "Connecting...",
"status.voice.connected": "Connected", "status.voice.disconnected": "Disconnected",
"action.call.mute": "Mute Microphone", "action.call.mute": "Mute Microphone",
"action.call.camera": "Camera", "action.call.camera": "Camera",
"action.call.screen": "Share Screen", "action.call.screen": "Share Screen",

View file

@ -302,7 +302,7 @@
"owner.transferred": "Własność przekazana pomyślnie", "owner.transferred": "Własność przekazana pomyślnie",
"title.voice.chat": "Czat głosowy", "title.voice.chat": "Czat głosowy",
"status.voice.connecting": "Łączenie...", "status.voice.connecting": "Łączenie...",
"status.voice.connected": "Połączono", "status.voice.disconnected": "Rozłączono",
"action.call.mute": "Wycisz mikrofon", "action.call.mute": "Wycisz mikrofon",
"action.call.camera": "Kamera", "action.call.camera": "Kamera",
"action.call.screen": "Udostępnij ekran", "action.call.screen": "Udostępnij ekran",

View file

@ -1340,6 +1340,15 @@ input:checked + .toggle-slider:before {
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
color: var(--text-color); 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 { .voice-title svg {
width: 1.1rem; width: 1.1rem;
@ -1351,10 +1360,12 @@ input:checked + .toggle-slider:before {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
font-size: 0.8rem; flex-shrink: 0;
font-weight: 600; font-size: 0.75rem;
font-weight: 500;
color: var(--darker-text-color); color: var(--darker-text-color);
opacity: 0.85; opacity: 0.75;
font-variant-numeric: tabular-nums;
} }
#voice-status .status-dot { #voice-status .status-dot {
width: 0.55rem; width: 0.55rem;
@ -1364,14 +1375,17 @@ input:checked + .toggle-slider:before {
background: var(--darker-text-color); background: var(--darker-text-color);
} }
#voice-status.connecting .status-dot { #voice-status.connecting .status-dot {
background: hsl(35, 90%, 55%);
animation: voice-dot-pulse 1.2s ease-in-out infinite; animation: voice-dot-pulse 1.2s ease-in-out infinite;
} }
#voice-status.connected {
opacity: 1;
}
#voice-status.connected .status-dot { #voice-status.connected .status-dot {
background: var(--big-green); 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 { @keyframes voice-dot-pulse {
50% { opacity: 0.25; } 50% { opacity: 0.25; }
@ -1414,7 +1428,7 @@ input:checked + .toggle-slider:before {
border-radius: 0.65rem; border-radius: 0.65rem;
overflow: hidden; overflow: hidden;
cursor: pointer; 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); box-shadow: 0 0.25rem 0.625rem rgba(0, 0, 0, 0.3);
transition: border-color 0.15s ease, box-shadow 0.2s ease; transition: border-color 0.15s ease, box-shadow 0.2s ease;
} }

View file

@ -44,13 +44,48 @@ function getStoredVolume(type, clientId) {
return v === undefined ? 100 : v; 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) { function setVoiceStatus(state) {
let el = document.getElementById("voice-status"); let el = document.getElementById("voice-status");
if (!el) return; if (!el) return;
el.classList.toggle("connected", state === "connected"); 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"); 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) { function setTileSpeaking(clientId, speaking) {
@ -208,6 +243,7 @@ async function startVoiceSession(serverUrl) {
showVoiceUI(); showVoiceUI();
updateVoiceUIUsers(); updateVoiceUIUsers();
setVoiceStatus("connecting"); setVoiceStatus("connecting");
updateVoiceChatTitle();
try { try {
const wsUrl = new URL(serverUrl); const wsUrl = new URL(serverUrl);
@ -234,8 +270,11 @@ async function startVoiceSession(serverUrl) {
try { try {
const baseUrl = currentVoiceWs.url.split('/ws')[0]; const baseUrl = currentVoiceWs.url.split('/ws')[0];
const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:'); const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:');
const t0 = performance.now();
const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`); const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`);
const usersRaw = await res.json(); const usersRaw = await res.json();
voicePingMs = Math.round(performance.now() - t0);
updateVoicePing();
const users = usersRaw.map(u => String(u)); const users = usersRaw.map(u => String(u));
let changed = (users.length !== voiceActiveUsers.length); let changed = (users.length !== voiceActiveUsers.length);
for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true; for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true;
@ -485,23 +524,46 @@ function handleCallUpdate(data) {
} }
async function resolveCallerInfo(roomId) { async function resolveCallerInfo(roomId) {
let name = window.dmUsernameCache ? window.dmUsernameCache[roomId] : null; if (window.fetchEncrypted) {
if (name && name.includes(':')) name = name.split(':')[0];
if (!name && window.fetchAsync) {
try { try {
let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${roomId}`); let infoPayload = await fetchEncrypted("group/info", roomId);
if (fetched && !fetched.startsWith("error:")) { if (infoPayload && infoPayload.startsWith("{")) {
name = fetched.includes(':') ? fetched.split(':')[0] : fetched; let info = JSON.parse(infoPayload);
if (window.dmUsernameCache) window.dmUsernameCache[roomId] = name; 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) {} } catch (e) {}
} }
let pfpUrl = window.dmPfpCache ? window.dmPfpCache[roomId] : null;
if (!pfpUrl && window.getAvatarUrl) { let targetId = String(roomId);
try { pfpUrl = await window.getAvatarUrl(roomId, name || "User"); } catch (e) {} let parts = targetId.split('_');
if (pfpUrl && window.dmPfpCache) window.dmPfpCache[roomId] = pfpUrl; 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) { 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() { function showVoiceUI() {
if (voiceUiContainer) return; if (voiceUiContainer) return;
@ -614,6 +709,7 @@ function showVoiceUI() {
</div> </div>
<div id="voice-status" class="connecting"> <div id="voice-status" class="connecting">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="status-ping" id="voice-ping"></span>
<span class="voice-status-text">${processBlah("status.voice.connecting")}</span> <span class="voice-status-text">${processBlah("status.voice.connecting")}</span>
</div> </div>
</div> </div>
@ -834,8 +930,8 @@ function updateVoiceUIUsers() {
} }
}); });
} }
if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; let resolvedName = authorName && authorName !== u ? authorName : null;
if (!authorName && u === String(window.id)) authorName = window.username; 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 avatarRes = window.dmPfpCache ? window.dmPfpCache[u] : null;
let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png"; let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png";
@ -852,7 +948,7 @@ function updateVoiceUIUsers() {
div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`; div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`;
let nameEl = document.getElementById(`voice-name-${u}`); 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 => { Array.from(container.children).forEach(child => {
@ -890,10 +986,13 @@ function updateVoiceUIUsers() {
} }
let authorName = window.dmUsernameCache ? window.dmUsernameCache[clientId] : null; 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}`); 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 => { Array.from(container.children).forEach(child => {
@ -1048,6 +1147,8 @@ function toggleVoiceMic() {
} }
function endVoiceSession() { function endVoiceSession() {
setVoiceStatus("disconnected");
if (currentVoiceCallId) { if (currentVoiceCallId) {
let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId }); let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId });
if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{}); if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{});