voice chat indicators

This commit is contained in:
olcxja 2026-08-25 22:46:49 +02:00
commit b12a75cfc4
8 changed files with 464 additions and 42 deletions

View file

@ -2674,6 +2674,8 @@ async function renderDms(res) {
let isRead = dmData.string3 === "true";
let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";
seedCallStateFromList(dmIdRaw, dmData.call === true);
let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27");
let unreadBadge = `<div class="unread-badge ${isRead ? "" : "show"}"></div>`;
@ -2686,6 +2688,7 @@ async function renderDms(res) {
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(targetUsername)} (${escapeHtml(targetId.split(';')[0])})</span>
</div>
${unreadBadge}
${getCallIndicatorHtml()}
</button>
`;
count++;
@ -2694,6 +2697,7 @@ async function renderDms(res) {
if (count > 0) {
container.innerHTML = html;
container.classList.remove("empty");
updateVoicePresenceUI();
} else {
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
container.classList.add("empty");
@ -2731,6 +2735,8 @@ async function renderGroups(res) {
let isRead = groupData.string3 === "true";
let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";
seedCallStateFromList(groupId, groupData.call === true);
let infoRes = await fetchEncrypted("group/info", groupId);
if (infoRes && infoRes.startsWith("{")) {
let info = JSON.parse(infoRes);
@ -2750,6 +2756,7 @@ async function renderGroups(res) {
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtmlText(groupId.replace(';', ':'))}</span>
</div>
${unreadBadge}
${getCallIndicatorHtml()}
</button>
`;
count++;
@ -2762,6 +2769,7 @@ async function renderGroups(res) {
if (count > 0) {
container.innerHTML = html;
container.classList.remove("empty");
updateVoicePresenceUI();
} else {
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.groups</blah></p>`;
container.classList.add("empty");
@ -2813,6 +2821,101 @@ function escapeHtmlText(value) {
return escapeHtml(String(value ?? ""));
}
// --- Wskaźniki aktywnej rozmowy głosowej (zielone słuchawki) ---
function getCallIndicatorHtml() {
return `<div class="call-indicator">${ICONS.headphones({ width: "1rem", height: "1rem", stroke: "var(--big-green)" })}</div>`;
}
function roomHasActiveCall(roomId) {
if (!roomId || !window.activeCallsByRoom) return false;
//Semantyka mapy: klucz ISTNIEJE = rozmowa trwa. Pusta tablica oznacza
//"aktywna, ale skład jeszcze nieznany" (seed z listy DM/grup).
return Array.isArray(window.activeCallsByRoom[sanitizeId(roomId)]);
}
//Zapisuje flagę "trwa rozmowa" z listy DM/grup (nie nadpisuje znanej listy
//uczestników z call_update - ta jest dokładniejsza).
function ensureActiveCallsMap() {
//Obiekt bez prototypu - klucze pochodzą z danych sieciowych (roomId), a
//zapis np. pod "__proto__" na zwykłym obiekcie zanieczyściłby prototyp
if (!window.activeCallsByRoom) window.activeCallsByRoom = Object.create(null);
return window.activeCallsByRoom;
}
function seedCallStateFromList(roomId, hasCall) {
if (!hasCall) return;
let map = ensureActiveCallsMap();
let key = sanitizeId(roomId);
if (!Array.isArray(map[key])) {
map[key] = [];
}
}
function toggleCallIndicator(rootEl, show) {
if (!rootEl) return;
let ind = rootEl.querySelector(".call-indicator");
if (ind) ind.classList.toggle("show", !!show);
}
//Odświeża wskaźniki rozmów: listy DM/grup, przyciski call w top-barach
//oraz wiersze członków w panelu details.
//roomIds = null -> pełna synchronizacja (po renderze list),
//roomIds = [id] -> tylko wskazane pokoje (ścieżka gorąca: call_update),
// żeby przy dużych listach nie skanować całego DOM.
function updateVoicePresenceUI(roomIds = null) {
let targets = Array.isArray(roomIds) ? roomIds.map(r => sanitizeId(r)) : null;
[["dms-list", "dm-btn-"], ["groups-list", "group-btn-"]].forEach(([listId, prefix]) => {
let container = document.getElementById(listId);
if (!container) return;
container.querySelectorAll(".room-entry").forEach(btn => {
if (!btn.id.startsWith(prefix)) return;
let roomId = btn.id.substring(prefix.length);
if (targets && !targets.includes(roomId)) return;
toggleCallIndicator(btn, roomHasActiveCall(roomId));
});
});
let currentKey = window.currentDmId ? sanitizeId(window.currentDmId) : null;
let currentAffected = targets === null || (currentKey !== null && targets.includes(currentKey));
if (!currentAffected) return;
let inCurrent = roomHasActiveCall(window.currentDmId);
document.querySelectorAll(".room-action-call svg").forEach(svg => {
svg.style.fill = inCurrent ? "var(--big-green)" : "";
});
if (currentKey) {
let users = (window.activeCallsByRoom && window.activeCallsByRoom[currentKey]) || [];
let detailsBar = document.getElementById("roomdetailsbar");
if (detailsBar) {
detailsBar.querySelectorAll(".room-entry[data-member-id]").forEach(btn => {
toggleCallIndicator(btn, users.includes(btn.getAttribute("data-member-id")));
});
}
}
}
//Pobiera aktualny skład rozmowy dla otwartego pokoju (snapshot po otwarciu -
//call_update przychodzi dopiero przy join/leave).
async function refreshRoomCallState() {
try {
if (!window.currentDmId || !window.currentRoomType) return;
let res = await fetchEncrypted("room/voice/state", JSON.stringify({
string1: window.currentRoomType,
string2: window.currentDmId
}));
if (!res || !res.startsWith("[") || !window.currentDmId) return;
let participants = JSON.parse(res).map(u => String(u).split(";")[0]);
let map = ensureActiveCallsMap();
let key = sanitizeId(window.currentDmId);
if (participants.length > 0) map[key] = participants;
else delete map[key];
updateVoicePresenceUI([key]);
} catch (e) {}
}
async function generateDmMembersHtml(targetId, targetUsername) {
let myIdStr = id + ";" + host;
let myUsername = await fetchAsync(`${url}/idtoname?id=${myIdStr}`);
@ -2831,19 +2934,21 @@ async function generateDmMembersHtml(targetId, targetUsername) {
<div class="sidebar-section-header" style="margin-top: 0.5rem; margin-bottom: 0.5rem; width: 100%;">
<span style="padding-left: 0.6rem; font-size: 0.8rem; font-weight: bold; opacity: 0.5; text-transform: uppercase;"><blah>title.members</blah></span>
</div>
<button class="room-entry" onclick="openProfile('${safeTargetId}')" style="margin-bottom: 0;">
<button class="room-entry" data-member-id="${sanitizeId(targetId.split(';')[0])}" onclick="openProfile('${safeTargetId}')" style="margin-bottom: 0;">
<img src="${targetPfp}" class="room-pfp">
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
<span class="room-name">${targetDisplayName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtmlText(targetUsername)}</span>
</div>
${getCallIndicatorHtml()}
</button>
<button class="room-entry" onclick="openProfile('${safeMyId}')" style="margin-bottom: 0;">
<button class="room-entry" data-member-id="${sanitizeId(String(id))}" onclick="openProfile('${safeMyId}')" style="margin-bottom: 0;">
<img src="${myPfp}" class="room-pfp">
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
<span class="room-name">${myDisplaySafe}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtmlText(myUsername)}</span>
</div>
${getCallIndicatorHtml()}
</button>
</div>
`;
@ -2972,14 +3077,16 @@ async function generateGroupMembersHtml(groupId, groupName) {
let memberDisplayName = escapeHtml(memberUsername.split(':')[0]);
let safeMemberId = sanitizeId(memberId);
let safeGroupId = sanitizeId(groupId);
let memberNumericId = sanitizeId(String(memberId).split(';')[0]);
html += `
<button class="room-entry" onclick="openProfile('${safeMemberId}')" oncontextmenu="handleMemberContextMenu(event, '${safeGroupId}', '${safeMemberId}')" style="margin-bottom: 0;">
<button class="room-entry" data-member-id="${memberNumericId}" onclick="openProfile('${safeMemberId}')" oncontextmenu="handleMemberContextMenu(event, '${safeGroupId}', '${safeMemberId}')" style="margin-bottom: 0;">
<img src="${memberPfp}" class="room-pfp">
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
<span class="room-name">${memberDisplayName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(memberUsername)}</span>
</div>
${getCallIndicatorHtml()}
</button>`;
}
@ -3067,6 +3174,9 @@ async function openDm(dmId, username, targetId, type = 'dms') {
}
await switchDetailsContent("title.details", membersHtml);
updateVoicePresenceUI();
refreshRoomCallState();
let msgContainer = document.getElementById("chat-messages");
if (msgContainer) {
msgContainer.innerHTML = `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.messages.loading</blah></div>`;