diff --git a/android/app/build.gradle b/android/app/build.gradle
index 11a28535..70f0a50e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "olcxja.miarven"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 8
- versionName "0.2.2"
+ versionCode 7
+ versionName "0.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/android/app/src/main/assets/public/icons.js b/android/app/src/main/assets/public/icons.js
index 497cb118..5f006339 100644
--- a/android/app/src/main/assets/public/icons.js
+++ b/android/app/src/main/assets/public/icons.js
@@ -55,7 +55,6 @@ const ICONS = {
video: (props) => ICONS._feather(``, props),
file: (props) => ICONS._feather(``, props),
mic: (props) => ICONS._feather(``, props),
- headphones: (props) => ICONS._feather(``, props),
speaker: (props) => ICONS._create(``, { viewBox: "0 0 24 24", width: "1.4rem", height: "1.4rem", fill: "var(--text-color)", stroke: "none" }, props),
home: (props) => ICONS._create(``, { viewBox: "-1 -1 18 18", width: "1.5rem", height: "1.5rem", fill: "none", stroke: "none" }, props),
diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js
index a5d0ca32..fa231a06 100644
--- a/android/app/src/main/assets/public/main.js
+++ b/android/app/src/main/assets/public/main.js
@@ -1149,20 +1149,12 @@ 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);
@@ -1170,15 +1162,11 @@ 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);
@@ -1569,7 +1557,7 @@ async function switchRoomsBar(title, content) {
await delay(200);
- roomsTopBarTransition.innerHTML = escapeHtmlText(processBlah(title));
+ roomsTopBarTransition.innerHTML = processBlah(title);
let parser = new DOMParser();
@@ -1648,10 +1636,10 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
let clickableContent = showRoomBar ?
`` :
`
- ${icon ? icon + '' : ''}${escapeHtmlText(processBlah(title))}
+ ${icon ? icon + '' : ''}${processBlah(title)}
`;
roomsTopBarTransition.innerHTML = `${backBtnHtml}${clickableContent}
${pinCallBtns}`;
@@ -1722,7 +1710,7 @@ async function switchDetailsContent(title, content, showBack = false, backAction
pinCallBtns = `${groupMenuBtnHtml}${pinCallBtns}`;
}
let backBtn = showBack ? `` : "";
- roomsTopBarTransition.innerHTML = `${backBtn}${escapeHtmlText(processBlah(title))}${pinCallBtns}`;
+ roomsTopBarTransition.innerHTML = `${backBtn}${processBlah(title)}${pinCallBtns}`;
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
@@ -2045,9 +2033,7 @@ async function openProfile(accountId) {
});
let roleRes = await fetchEncrypted("room/role", payload);
if (roleRes && !roleRes.startsWith("error:")) {
- //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, "_");
+ let mappedRole = "role." + roleRes.replace(" ", "_");
roleHtml = `${mappedRole}
`;
}
}
@@ -2289,7 +2275,7 @@ async function renderInvites(res, type) {
}
let entry = invitesEntry
- .replaceAll("{username}", escapeHtmlText(groupName))
+ .replaceAll("{username}", groupName)
.replaceAll("{pfp}", pfp)
.replaceAll("{desc}", `${desc}`)
.replaceAll("{actions}", actions)
@@ -2658,13 +2644,7 @@ 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;
@@ -2685,13 +2665,8 @@ async function renderDms(res) {
let myId = `${id};${host}`;
let targetId = sanitizeId(parts[0] === myId ? parts[1] : parts[0]);
- //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 targetUsername = await fetchAsync(`${url}/idtoname?id=${targetId}`);
+ if (!targetUsername || targetUsername.trim() === "" || targetUsername.startsWith("error:")) continue;
let pfp = await getAvatarUrl(targetId, targetUsername);
let displayName = escapeHtml(targetUsername.split(':')[0]);
@@ -2699,8 +2674,6 @@ 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 = ``;
@@ -2713,19 +2686,15 @@ async function renderDms(res) {
${escapeHtml(targetUsername)} (${escapeHtml(targetId.split(';')[0])})
${unreadBadge}
- ${getCallIndicatorHtml()}
`;
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 = `desc.no.dms
`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
@@ -2743,17 +2712,7 @@ 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;
@@ -2772,26 +2731,11 @@ 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);
+ 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);
@@ -2806,20 +2750,19 @@ async function renderGroups(res) {
${escapeHtmlText(groupId.replace(';', ':'))}
${unreadBadge}
- ${getCallIndicatorHtml()}
`;
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 = `desc.no.groups
`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
@@ -2870,101 +2813,6 @@ function escapeHtmlText(value) {
return escapeHtml(String(value ?? ""));
}
-// --- Wskaźniki aktywnej rozmowy głosowej (zielone słuchawki) ---
-
-function getCallIndicatorHtml() {
- return `${ICONS.headphones({ width: "1rem", height: "1rem", stroke: "var(--big-green)" })}
`;
-}
-
-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}`);
@@ -2983,21 +2831,19 @@ async function generateDmMembersHtml(targetId, targetUsername) {
-