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(()=>{});