This commit is contained in:
olcxja 2026-08-26 00:11:00 +02:00
commit b2a6c19bc0
4 changed files with 554 additions and 116 deletions

View file

@ -1149,12 +1149,20 @@ function createAvatarSvg(name, size = 512) {
}
async function getAvatarUrl(id, username) {
//S22: helper robi fetch istnienia pliku - bez cache każde odświeżenie listy
//generowało 1-2 żądania NA wiersz (główny składnik N+1).
//Cache'owane TYLKO pozytywne potwierdzenia - nieudane lookupy (chwilowy
//network itp.) nie mogą przyklejać fallback-SVG na minutę
if (!window.pfpExistenceCache) window.pfpExistenceCache = {};
const cached = window.pfpExistenceCache[id];
if (cached && Date.now() - cached.ts < 60000) return cached.url;
try {
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
if (id === window.id && window.profileUpdateTimestamp) pfpUrl += `&t=${window.profileUpdateTimestamp}`;
if ((await fetchAsync(pfpUrl)) == "") {
throw Error();
}
window.pfpExistenceCache[id] = { url: pfpUrl, ts: Date.now() };
return pfpUrl;
} catch (e) {
return createAvatarSvg(username);
@ -1162,11 +1170,15 @@ async function getAvatarUrl(id, username) {
}
async function getGroupAvatarUrl(groupId, groupName) {
if (!window.groupPfpExistenceCache) window.groupPfpExistenceCache = {};
const cached = window.groupPfpExistenceCache[groupId];
if (cached && Date.now() - cached.ts < 60000) return cached.url;
try {
let pfpUrl = `${url}/group/storage/public/getentry?id=${groupId}&e=larp.profile.pfp`;
if ((await fetchAsync(pfpUrl)) == "") {
throw Error();
}
window.groupPfpExistenceCache[groupId] = { url: pfpUrl, ts: Date.now() };
return pfpUrl;
} catch (e) {
return createAvatarSvg(groupName);
@ -1557,7 +1569,7 @@ async function switchRoomsBar(title, content) {
await delay(200);
roomsTopBarTransition.innerHTML = processBlah(title);
roomsTopBarTransition.innerHTML = escapeHtmlText(processBlah(title));
let parser = new DOMParser();
@ -1636,10 +1648,10 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
let clickableContent = showRoomBar ?
`<button id="room-topbar-clickable" class="inherit" onclick="mobileNavDetails();" style="cursor: pointer; display: flex; align-items: center; border-radius: 0.65rem; background: transparent; outline: none; height: 100%; text-align: left; min-width: 0; width: auto; max-width: 100%;">
${icon ? icon + '<space></space>' : ''}<inherit style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; min-width: 0; width: auto;">${processBlah(title)}</inherit>
${icon ? icon + '<space></space>' : ''}<inherit style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; min-width: 0; width: auto;">${escapeHtmlText(processBlah(title))}</inherit>
</button>` :
`<inherit style="display: flex; align-items: center; height: 100%; min-width: 0; width: auto; max-width: 100%;">
${icon ? icon + '<space></space>' : ''}<inherit style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; min-width: 0; width: auto;">${processBlah(title)}</inherit>
${icon ? icon + '<space></space>' : ''}<inherit style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; min-width: 0; width: auto;">${escapeHtmlText(processBlah(title))}</inherit>
</inherit>`;
roomsTopBarTransition.innerHTML = `${backBtnHtml}${clickableContent}<div class="flex-spacer"></div>${pinCallBtns}`;
@ -1710,7 +1722,7 @@ async function switchDetailsContent(title, content, showBack = false, backAction
pinCallBtns = `${groupMenuBtnHtml}${pinCallBtns}`;
}
let backBtn = showBack ? `<button class="topbar-action-btn" onclick="${backAction}" style="margin-right: 0.5rem">${ICONS.arrowBack({ width: "1.5rem", height: "1.5rem" })}</button>` : "";
roomsTopBarTransition.innerHTML = `<inherit style="display: flex; align-items: center;">${backBtn}<inherit>${processBlah(title)}</inherit></inherit><div class="flex-spacer"></div>${pinCallBtns}`;
roomsTopBarTransition.innerHTML = `<inherit style="display: flex; align-items: center;">${backBtn}<inherit>${escapeHtmlText(processBlah(title))}</inherit></inherit><div class="flex-spacer"></div>${pinCallBtns}`;
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
@ -2033,7 +2045,9 @@ async function openProfile(accountId) {
});
let roleRes = await fetchEncrypted("room/role", payload);
if (roleRes && !roleRes.startsWith("error:")) {
let mappedRole = "role." + roleRes.replace(" ", "_");
//roleRes trafia do innerHTML - whitelist znaków zamiast surowej
//podstawienia (audyt W6: serwer-controlled string w szablonie)
let mappedRole = "role." + String(roleRes).replace(/[^A-Za-z0-9_.]/g, "_");
roleHtml = `<div class="profile-role" style="opacity:0.7; font-size:0.9rem; margin-top:0.3rem;"><blah>${mappedRole}</blah></div>`;
}
}
@ -2275,7 +2289,7 @@ async function renderInvites(res, type) {
}
let entry = invitesEntry
.replaceAll("{username}", groupName)
.replaceAll("{username}", escapeHtmlText(groupName))
.replaceAll("{pfp}", pfp)
.replaceAll("{desc}", `<blah>${desc}</blah>`)
.replaceAll("{actions}", actions)
@ -2644,7 +2658,13 @@ function handleMobileSwipe() {
}
}
//S22: generacje renderów - przy nakładających się refreshDms/refreshGroups
//(ws events, fallbacki) starszy wynik nie może nadpisać nowszego innerHTML-em
let dmListRenderGen = 0;
let groupListRenderGen = 0;
async function renderDms(res) {
const gen = ++dmListRenderGen;
let container = document.getElementById("dms-list");
if (!container) return;
@ -2665,8 +2685,13 @@ async function renderDms(res) {
let myId = `${id};${host}`;
let targetId = sanitizeId(parts[0] === myId ? parts[1] : parts[0]);
let targetUsername = await fetchAsync(`${url}/idtoname?id=${targetId}`);
if (!targetUsername || targetUsername.trim() === "" || targetUsername.startsWith("error:")) continue;
//S22: cache nazw - idtoname było wołane od nowa przy każdym renderze
let targetUsername = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null;
if (!targetUsername) {
targetUsername = await fetchAsync(`${url}/idtoname?id=${targetId}`);
if (!targetUsername || targetUsername.trim() === "" || targetUsername.startsWith("error:")) continue;
if (window.dmUsernameCache) window.dmUsernameCache[targetId] = targetUsername;
}
let pfp = await getAvatarUrl(targetId, targetUsername);
let displayName = escapeHtml(targetUsername.split(':')[0]);
@ -2694,11 +2719,13 @@ async function renderDms(res) {
count++;
}
if (gen !== dmListRenderGen) return; //nowszy render przejął listę
if (count > 0) {
container.innerHTML = html;
container.classList.remove("empty");
updateVoicePresenceUI();
} else {
if (gen !== dmListRenderGen) return;
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
@ -2716,7 +2743,17 @@ async function renderDms(res) {
}
}
//S22: TTL-cache nazw grup (60 s) - nazwy rzadko się zmieniają,
//a group/info było najdroższym elementem renderu listy grup
function getGroupNameFromCache(groupId) {
if (!window.groupNameCache) return null;
let entry = window.groupNameCache[sanitizeId(groupId)];
if (!entry || Date.now() - entry.ts > 60000) return null;
return entry.name;
}
async function renderGroups(res) {
const gen = ++groupListRenderGen;
let container = document.getElementById("groups-list");
if (!container) return;
@ -2737,11 +2774,24 @@ async function renderGroups(res) {
seedCallStateFromList(groupId, groupData.call === true);
let infoRes = await fetchEncrypted("group/info", groupId);
if (infoRes && infoRes.startsWith("{")) {
let info = JSON.parse(infoRes);
let groupName = info.string2;
//S22: TTL-cache nazw grup - group/info było wołane per grupa per render
let groupName = getGroupNameFromCache(groupId);
if (groupName === null) {
let infoRes = await fetchEncrypted("group/info", groupId);
if (infoRes && infoRes.startsWith("{")) {
groupName = JSON.parse(infoRes).string2;
if (!window.groupNameCache) window.groupNameCache = {};
window.groupNameCache[sanitizeId(groupId)] = { name: groupName, ts: Date.now() };
} else {
if (infoRes === "error:group.not.found") {
//grupa została usunięta - wyczyść nieaktualny wpis z listy
removeRoomFromCachedList(groupId, 'groups');
}
continue;
}
}
{
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
let pfp = await getGroupAvatarUrl(groupId, groupName);
@ -2760,17 +2810,16 @@ async function renderGroups(res) {
</button>
`;
count++;
} else if (infoRes === "error:group.not.found") {
//grupa została usunięta - wyczyść nieaktualny wpis z listy
removeRoomFromCachedList(groupId, 'groups');
}
}
if (gen !== groupListRenderGen) return; //nowszy render przejął listę
if (count > 0) {
container.innerHTML = html;
container.classList.remove("empty");
updateVoicePresenceUI();
} else {
if (gen !== groupListRenderGen) return;
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.groups</blah></p>`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");