fix group call popup & improve vc topbar design
This commit is contained in:
parent
135fbf23ca
commit
88dde78581
10 changed files with 286 additions and 56 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
<div id="voice-status" class="connecting">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-ping" id="voice-ping"></span>
|
||||
<span class="voice-status-text">${processBlah("status.voice.connecting")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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(()=>{});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
141
webroot/voice.js
141
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() {
|
|||
</div>
|
||||
<div id="voice-status" class="connecting">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-ping" id="voice-ping"></span>
|
||||
<span class="voice-status-text">${processBlah("status.voice.connecting")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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(()=>{});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue