forked from olcxjas-softworks/LarpixClient
massive security update
This commit is contained in:
parent
cab6314bc7
commit
1593a3a33a
20 changed files with 1121 additions and 339 deletions
385
webroot/main.js
385
webroot/main.js
|
|
@ -83,7 +83,9 @@ async function packetEncPass(pass, key, accountId) {
|
|||
async function getNonce(accountId, key) {
|
||||
|
||||
let nonce;
|
||||
let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`)).text();
|
||||
let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`, {
|
||||
headers: {"X-Crypto": "g1"}
|
||||
})).text();
|
||||
|
||||
try {
|
||||
nonce = await decryptString(fetchRes, key);
|
||||
|
|
@ -123,7 +125,8 @@ async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBa
|
|||
|
||||
function keyDataFromServerJson(jsonFromServer) {
|
||||
const data = JSON.parse(jsonFromServer);
|
||||
return [data.pubX25519, data.pubMlKem, data.idKey]
|
||||
//0=pubX25519, 1=pubMlKem, 2=idKey, 3=kdfSalt (wymagany do PBKDF2 przy rejestracji)
|
||||
return [data.pubX25519, data.pubMlKem, data.idKey, data.kdfSalt]
|
||||
}
|
||||
|
||||
function base64ToUint8(base64) {
|
||||
|
|
@ -705,25 +708,20 @@ async function decryptBytes(combinedBytes, passphrase) {
|
|||
|
||||
async function encryptString(plainText, passphrase) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(plainText);
|
||||
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
|
||||
'raw', pwHash, {name: 'AES-GCM'}, false, ['encrypt']
|
||||
);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{name: 'AES-CBC', iv: iv},
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = new Uint8Array(await crypto.subtle.encrypt(
|
||||
{name: 'AES-GCM', iv: iv},
|
||||
key,
|
||||
data
|
||||
);
|
||||
encoder.encode(plainText)
|
||||
));
|
||||
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(encrypted), iv.length);
|
||||
|
||||
return uint8ToBase64(combined);
|
||||
return "G1:" + uint8ToBase64(concatUint8(iv, encrypted));
|
||||
}
|
||||
|
||||
async function encryptBytesWithNonce(dataUint8, key, nonce) {
|
||||
|
|
@ -732,37 +730,44 @@ async function encryptBytesWithNonce(dataUint8, key, nonce) {
|
|||
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
|
||||
|
||||
const importedKey = await crypto.subtle.importKey(
|
||||
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
|
||||
'raw', pwHash, {name: 'AES-GCM'}, false, ['encrypt']
|
||||
);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{name: 'AES-CBC', iv: iv},
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = new Uint8Array(await crypto.subtle.encrypt(
|
||||
{name: 'AES-GCM', iv: iv},
|
||||
importedKey,
|
||||
dataUint8
|
||||
);
|
||||
));
|
||||
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(encrypted), iv.length);
|
||||
const prefix = encoder.encode("G1:");
|
||||
const combined = new Uint8Array(prefix.length + iv.length + encrypted.byteLength);
|
||||
combined.set(prefix, 0);
|
||||
combined.set(iv, prefix.length);
|
||||
combined.set(encrypted, prefix.length + iv.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
async function decryptString(base64Text, passphrase) {
|
||||
//tylko AES-GCM ("G1:") - pakiety bez prefiksu odrzucane
|
||||
if (typeof base64Text !== "string" || !base64Text.startsWith("G1:")) {
|
||||
throw new Error("error:invalid.cipher.format");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
|
||||
const combined = base64ToUint8(base64Text.substring(3));
|
||||
|
||||
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', pwHash, {name: 'AES-CBC'}, false, ['decrypt']
|
||||
'raw', pwHash, {name: 'AES-GCM'}, false, ['decrypt']
|
||||
);
|
||||
|
||||
const iv = combined.slice(0, 16);
|
||||
const data = combined.slice(16);
|
||||
const iv = combined.slice(0, 12);
|
||||
const data = combined.slice(12);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{name: 'AES-CBC', iv: iv},
|
||||
{name: 'AES-GCM', iv: iv},
|
||||
key,
|
||||
data
|
||||
);
|
||||
|
|
@ -784,6 +789,7 @@ async function fetchPost(url, value) {
|
|||
method: "POST",
|
||||
body: value,
|
||||
headers: {
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
|
||||
}
|
||||
});
|
||||
|
|
@ -806,6 +812,7 @@ async function fetchPostEnc(url, value) {
|
|||
method: "POST",
|
||||
body: await encryptWithNonce(value, passwordHash, nonce),
|
||||
headers: {
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
}
|
||||
});
|
||||
|
|
@ -835,6 +842,7 @@ async function fetchAsyncWAuth(url) {
|
|||
let response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
|
||||
}
|
||||
});
|
||||
|
|
@ -864,17 +872,30 @@ async function Auth(loginUsername, loginPassword) {
|
|||
}
|
||||
let actualId = resolvedId.split(":")[0];
|
||||
|
||||
let passwordHash = await hashSHA3_512(loginPassword);
|
||||
//sól KDF jest wymagana - konta bez soli nie mogą się zalogować
|
||||
let saltRes = await fetchAsync(`${url}/kdfsalt?id=${actualId}`);
|
||||
if (!saltRes || !saltRes.startsWith("P:")) {
|
||||
return "error:invalid.username.or.password";
|
||||
}
|
||||
let credHash = await deriveCredentialKdf(loginPassword, saltRes.substring(2).trim());
|
||||
|
||||
let response = await fetch(`${url}/auth?id=${actualId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(credHash, credHash, await getNonce(actualId, credHash))
|
||||
}
|
||||
});
|
||||
|
||||
let data = await response.text();
|
||||
if (data.startsWith("success:")) {
|
||||
data += "|" + actualId;
|
||||
//utrwal globale sesyjne - dalsze żądania w tej sesji mają od razu ważny hash
|
||||
passwordHash = credHash;
|
||||
if (!id) {
|
||||
id = actualId;
|
||||
try { localStorage.setItem("id", actualId); } catch (e) {}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} finally {
|
||||
|
|
@ -902,6 +923,7 @@ async function fetchEncrypted(request, body = "") {
|
|||
|
||||
, passwordHash, nonce),
|
||||
headers: {
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
}
|
||||
});
|
||||
|
|
@ -929,6 +951,7 @@ async function fetchEncryptedUpload(target, fileBytesUint8) {
|
|||
method: "POST",
|
||||
body: await encryptBytesWithNonce(fileBytesUint8, passwordHash, nonce),
|
||||
headers: {
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce),
|
||||
"target": await encryptWithNonce(target, passwordHash, nonce)
|
||||
}
|
||||
|
|
@ -1046,13 +1069,35 @@ function clearAction(actionid) {
|
|||
}
|
||||
|
||||
|
||||
async function hashSHA3_512(input) {
|
||||
//solony, wolny KDF - 600k iteracji PBKDF2-SHA256, wyjscie 512 bitow (128 hex)
|
||||
async function deriveCredentialKdf(passwordPlain, saltB64) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(input);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-512', data); //-3 kiedys xddddddddddddddddd
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return hashHex;
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(passwordPlain),
|
||||
{name: "PBKDF2"},
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
const bits = await crypto.subtle.deriveBits(
|
||||
{name: "PBKDF2", hash: "SHA-256", salt: base64ToUint8(saltB64), iterations: 600000},
|
||||
keyMaterial,
|
||||
512
|
||||
);
|
||||
return Array.from(new Uint8Array(bits)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
//zwraca null gdy brak konta/soli - celowo NIE rzuca, żeby nie wywalać bootu aplikacji
|
||||
//(strona logowania i świeży użytkownik nie mają soli; start() sam zrobi Auth i przekieruje)
|
||||
async function resolveCredentialHash(accountId) {
|
||||
if (!accountId) return null;
|
||||
try {
|
||||
let res = await fetchAsync(`${url}/kdfsalt?id=${accountId}`);
|
||||
if (res && res.startsWith("P:") && password) {
|
||||
return await deriveCredentialKdf(password, res.substring(2).trim());
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
//placeholder pfp
|
||||
|
|
@ -1402,9 +1447,10 @@ async function mainJS() {
|
|||
}
|
||||
|
||||
await initBlahs();
|
||||
await loadEmotes();
|
||||
|
||||
passwordHash = await hashSHA3_512(password);
|
||||
//emotes.json leży w katalogu głównym - na stronie /login fetch by trafił w HTML
|
||||
if (!window.location.pathname.includes("/login")) {
|
||||
await loadEmotes();
|
||||
}
|
||||
|
||||
if (host) {
|
||||
await updateProtocolAndUrl(host);
|
||||
|
|
@ -1420,6 +1466,12 @@ async function mainJS() {
|
|||
}
|
||||
}
|
||||
|
||||
//hash tylko dla istniejącej sesji; brak soli/konta nie może blokować startu -
|
||||
//start() i tak wykonuje własny Auth i przekieruje niezalogowanych na login
|
||||
if (id && password) {
|
||||
try { passwordHash = await resolveCredentialHash(id); } catch (e) { passwordHash = null; }
|
||||
}
|
||||
|
||||
await start();
|
||||
}
|
||||
|
||||
|
|
@ -1873,6 +1925,7 @@ async function openProfile(accountId) {
|
|||
|
||||
try {
|
||||
if (!accountId) accountId = window.id; //var fallback
|
||||
accountId = sanitizeId(accountId);
|
||||
|
||||
if (accountId.endsWith(';' + host)) {
|
||||
accountId = accountId.substring(0, accountId.length - host.length - 1);
|
||||
|
|
@ -1896,7 +1949,7 @@ async function openProfile(accountId) {
|
|||
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.bio`);
|
||||
if (bio.startsWith("error:")) bio = "";
|
||||
|
||||
let bioHtml = bio && bio.trim() !== "" ? `<div class="profile-bio">${bio.replace(/</g, "<").replace(/>/g, ">")}</div>` : "";
|
||||
let bioHtml = bio && bio.trim() !== "" ? `<div class="profile-bio">${escapeHtml(bio)}</div>` : "";
|
||||
|
||||
let isOwnProfile = accountId === id;
|
||||
let actionBtnHtml = "";
|
||||
|
|
@ -1992,8 +2045,8 @@ async function openProfile(accountId) {
|
|||
</div>
|
||||
<div class="profile-info">
|
||||
<div>
|
||||
<div class="profile-name">${profileName}</div>
|
||||
<div class="profile-id-host">${profileName}:${profileHost} (${accountId.split(':')[0].split(';')[0]})</div>
|
||||
<div class="profile-name">${escapeHtml(profileName)}</div>
|
||||
<div class="profile-id-host">${escapeHtml(profileName)}:${escapeHtml(profileHost)} (${escapeHtml(accountId.split(':')[0].split(';')[0])})</div>
|
||||
${roleHtml}
|
||||
</div>
|
||||
${bioHtml}
|
||||
|
|
@ -2129,7 +2182,7 @@ async function renderInvites(res, type) {
|
|||
|
||||
|
||||
if (invite.type === "dm") {
|
||||
let id = invite.id;
|
||||
let id = sanitizeId(invite.id);
|
||||
|
||||
if (!id.includes(":")) {
|
||||
id += `:${host}`;
|
||||
|
|
@ -2144,7 +2197,7 @@ async function renderInvites(res, type) {
|
|||
let actions = "";
|
||||
let desc = "";
|
||||
|
||||
let displayName = username || onlyId;
|
||||
let displayName = escapeHtml(username || onlyId);
|
||||
if (type === "received") {
|
||||
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
||||
actions = `
|
||||
|
|
@ -2174,8 +2227,8 @@ async function renderInvites(res, type) {
|
|||
|
||||
html += entry;
|
||||
} else if (invite.type === "group") {
|
||||
let groupId = invite.groupId || invite.id;
|
||||
let targetId = (type === "received") ? invite.sender : invite.receiver;
|
||||
let groupId = sanitizeId(invite.groupId || invite.id);
|
||||
let targetId = sanitizeId((type === "received") ? invite.sender : invite.receiver);
|
||||
|
||||
if (!targetId.includes(":")) {
|
||||
targetId += `:${host}`;
|
||||
|
|
@ -2198,9 +2251,9 @@ async function renderInvites(res, type) {
|
|||
let actions = "";
|
||||
let desc = "";
|
||||
|
||||
let displayName = username || onlyId;
|
||||
let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`;
|
||||
let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`;
|
||||
let displayName = escapeHtml(username || onlyId);
|
||||
let val1 = `<span onclick="openProfile('${sanitizeId(targetId)}')" style="cursor:pointer"><b>${displayName}</b></span>`;
|
||||
let val2 = `<span onclick="openProfile('${sanitizeId(targetId)}')" style="cursor:pointer"><b>${escapeHtml(onlyId)}</b></span>`;
|
||||
|
||||
if (type === "received") {
|
||||
desc = `:desc.invite.group.received:${val1};${val2}`;
|
||||
|
|
@ -2606,16 +2659,17 @@ async function renderDms(res) {
|
|||
let html = "";
|
||||
let count = 0;
|
||||
|
||||
for (let [dmId, dmData] of dmEntries) {
|
||||
for (let [dmIdRaw, dmData] of dmEntries) {
|
||||
let dmId = sanitizeId(dmIdRaw);
|
||||
let parts = dmId.split('_');
|
||||
let myId = `${id};${host}`;
|
||||
let targetId = parts[0] === myId ? parts[1] : parts[0];
|
||||
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;
|
||||
|
||||
let pfp = await getAvatarUrl(targetId, targetUsername);
|
||||
let displayName = targetUsername.split(':')[0];
|
||||
let displayName = escapeHtml(targetUsername.split(':')[0]);
|
||||
|
||||
let isRead = dmData.string3 === "true";
|
||||
let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";
|
||||
|
|
@ -2629,7 +2683,7 @@ async function renderDms(res) {
|
|||
<img src="${pfp}" 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" style="${nameStyle}">${displayName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername} (${targetId.split(';')[0]})</span>
|
||||
<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}
|
||||
</button>
|
||||
|
|
@ -2685,19 +2739,23 @@ async function renderGroups(res) {
|
|||
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
|
||||
let pfp = await getGroupAvatarUrl(groupId, groupName);
|
||||
|
||||
let safeGroupId = sanitizeId(groupId);
|
||||
let unreadBadge = `<div class="unread-badge ${isRead ? "" : "show"}"></div>`;
|
||||
|
||||
html += `
|
||||
<button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')" oncontextmenu="handleGroupContextMenu(event, '${groupId}')">
|
||||
<button class="room-entry" id="group-btn-${safeGroupId}" onclick="openDm('${safeGroupId}', decodeURIComponent('${safeGroupName}'), '${safeGroupId}', 'groups')" oncontextmenu="handleGroupContextMenu(event, '${safeGroupId}')">
|
||||
<img src="${pfp}" 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" style="${nameStyle}">${groupName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span>
|
||||
<span class="room-name" style="${nameStyle}">${escapeHtmlText(groupName)}</span>
|
||||
<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}
|
||||
</button>
|
||||
`;
|
||||
count++;
|
||||
} else if (infoRes === "error:group.not.found") {
|
||||
//grupa została usunięta - wyczyść nieaktualny wpis z listy
|
||||
removeRoomFromCachedList(groupId, 'groups');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2743,6 +2801,18 @@ function escapeHtml(unsafe) {
|
|||
.replaceAll(/'/g, "'");
|
||||
}
|
||||
|
||||
//ID-y/identyfikatory wstawiane do atrybutów HTML i inline JS (onclick).
|
||||
//Usuwa wszystko poza bezpiecznym zestawem znakow - nie da się wyjść z stringa JS ani atrybutu.
|
||||
//Dozwolone: litery, cyfry oraz _ . ; : @ -
|
||||
function sanitizeId(value) {
|
||||
return String(value ?? "").replace(/[^A-Za-z0-9_.;:@-]/g, "");
|
||||
}
|
||||
|
||||
//nazwy (grup itp.) wstawiane do treści HTML
|
||||
function escapeHtmlText(value) {
|
||||
return escapeHtml(String(value ?? ""));
|
||||
}
|
||||
|
||||
async function generateDmMembersHtml(targetId, targetUsername) {
|
||||
let myIdStr = id + ";" + host;
|
||||
let myUsername = await fetchAsync(`${url}/idtoname?id=${myIdStr}`);
|
||||
|
|
@ -2751,24 +2821,28 @@ async function generateDmMembersHtml(targetId, targetUsername) {
|
|||
let targetPfp = await getAvatarUrl(targetId, targetUsername);
|
||||
let targetDisplayName = targetUsername.split(':')[0];
|
||||
let myDisplayName = myUsername.split(':')[0];
|
||||
let safeTargetId = sanitizeId(targetId);
|
||||
let safeMyId = sanitizeId(myIdStr);
|
||||
targetDisplayName = escapeHtmlText(targetDisplayName);
|
||||
let myDisplaySafe = escapeHtmlText(myDisplayName);
|
||||
|
||||
return `
|
||||
<div class="list-container no-flex-grow" style="padding: 0 0.5rem; width: 100%;">
|
||||
<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('${targetId}')" style="margin-bottom: 0;">
|
||||
<button class="room-entry" 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;">${targetUsername}</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>
|
||||
</button>
|
||||
<button class="room-entry" onclick="openProfile('${myIdStr}')" style="margin-bottom: 0;">
|
||||
<button class="room-entry" 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">${myDisplayName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${myUsername}</span>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -2885,7 +2959,7 @@ async function generateGroupMembersHtml(groupId, groupName) {
|
|||
<div class="sidebar-section-header" style="margin-top: 0.5rem; margin-bottom: 0.5rem; width: 100%; display: flex; align-items: center;">
|
||||
<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 class="flex-spacer"></div>
|
||||
<button class="topbar-action-btn" onclick="inviteToGroupPrompt('${groupId}')" style="margin-right: 0.5rem; width: 1.2rem; height: 1.2rem; display: flex; align-items: center; justify-content: center; opacity: 0.7;">
|
||||
<button class="topbar-action-btn" onclick="inviteToGroupPrompt('${sanitizeId(groupId)}')" style="margin-right: 0.5rem; width: 1.2rem; height: 1.2rem; display: flex; align-items: center; justify-content: center; opacity: 0.7;">
|
||||
${ICONS.plus({ width: "1rem", height: "1rem" })}
|
||||
</button>
|
||||
</div>`;
|
||||
|
|
@ -2895,14 +2969,16 @@ async function generateGroupMembersHtml(groupId, groupName) {
|
|||
if (!memberUsername || memberUsername.startsWith("error")) continue;
|
||||
|
||||
let memberPfp = await getAvatarUrl(memberId, memberUsername);
|
||||
let memberDisplayName = memberUsername.split(':')[0];
|
||||
let memberDisplayName = escapeHtml(memberUsername.split(':')[0]);
|
||||
let safeMemberId = sanitizeId(memberId);
|
||||
let safeGroupId = sanitizeId(groupId);
|
||||
|
||||
html += `
|
||||
<button class="room-entry" onclick="openProfile('${memberId}')" oncontextmenu="handleMemberContextMenu(event, '${groupId}', '${memberId}')" style="margin-bottom: 0;">
|
||||
<button class="room-entry" 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;">${memberUsername}</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>
|
||||
</button>`;
|
||||
}
|
||||
|
|
@ -2925,6 +3001,16 @@ async function openDm(dmId, username, targetId, type = 'dms') {
|
|||
}
|
||||
|
||||
if (type === 'dms') {
|
||||
let keyCheck = await fetchEncrypted("dm/key/get", dmId);
|
||||
if (keyCheck === "error:forbidden") {
|
||||
//wpis w liscie do ktorego nie mamy dostepu - usun go i nie otwieraj rozmowy
|
||||
showBlahNotification(keyCheck);
|
||||
removeRoomFromCachedList(dmId, 'dms');
|
||||
removeRoomButton(sanitizeId(dmId), 'dms');
|
||||
refreshDms(0, false);
|
||||
clearAction("dmopen");
|
||||
return;
|
||||
}
|
||||
currentDmKey = await ensureDmRoomKey(dmId);
|
||||
if (!currentDmKey) {
|
||||
throw new Error("Missing DM room key");
|
||||
|
|
@ -2936,8 +3022,16 @@ async function openDm(dmId, username, targetId, type = 'dms') {
|
|||
window.currentGroupKeyIndex = info.string4 || "0";
|
||||
window.currentGroupOwner = info.string7;
|
||||
} else {
|
||||
window.currentGroupKeyIndex = "0";
|
||||
window.currentGroupOwner = "";
|
||||
showBlahNotification(infoPayload && infoPayload.startsWith("error:") ? infoPayload : "error:group.not.found");
|
||||
//usuń wpis z listy tylko przy deterministycznym błędzie
|
||||
//(network.error itp. nie może czyścić poprawnych wpisów)
|
||||
if (infoPayload === "error:group.not.found") {
|
||||
removeRoomFromCachedList(dmId, 'groups');
|
||||
removeRoomButton(sanitizeId(dmId), 'groups');
|
||||
refreshGroups(0, false);
|
||||
}
|
||||
clearAction("dmopen");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3015,13 +3109,9 @@ async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
|||
|
||||
if (res && res.startsWith("error:")) {
|
||||
if (res === "error:forbidden" || res === "error:not.a.member" || res === "error:group.not.found") {
|
||||
if (dmMessagePollInterval) clearInterval(dmMessagePollInterval);
|
||||
currentDmId = null;
|
||||
showBlahNotification(res);
|
||||
if (mainScreen.classList.contains('mobile-details')) {
|
||||
mainScreen.classList.remove('mobile-details');
|
||||
}
|
||||
switchDetailsContent("title.details", `<div style="text-align: center; opacity: 0.5; padding: 1rem; margin-top: 2rem;"><blah>desc.no.chat.selected</blah></div>`);
|
||||
//brak dostępu do pokoju - serwer usunął też nieaktualny wpis z listy,
|
||||
//my dodatkowo czyścimy cache/UI i wychodzimy z ekranu rozmowy
|
||||
await handleLostRoomAccess(dmId, null, res);
|
||||
return;
|
||||
}
|
||||
showBlahNotification(res);
|
||||
|
|
@ -3143,7 +3233,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
}
|
||||
|
||||
let attContainerId = `att-container-${msgId}-${att.id}`;
|
||||
let safeAttId = sanitizeId(att.id);
|
||||
let attContainerId = `att-container-${sanitizeId(String(msgId))}-${safeAttId}`;
|
||||
let isMedia = att.type && (att.type.startsWith("image/") || att.type.startsWith("video/"));
|
||||
let sizeStr = att.size ? formatBytes(att.size) : "";
|
||||
let displayName = truncateFilename(att.name, 25);
|
||||
|
|
@ -3154,7 +3245,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let safeType = encodeURIComponent(att.type).replace(/'/g, "%27");
|
||||
let safeName = encodeURIComponent(att.name).replace(/'/g, "%27");
|
||||
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 7.5rem; max-width: 18.75rem; min-height: 6.25rem; max-height: 18.75rem; background: rgba(0,0,0,0.2); border: 0.0625rem solid var(--border-color); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'), '${msg.key || "0"}')">
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 7.5rem; max-width: 18.75rem; min-height: 6.25rem; max-height: 18.75rem; background: rgba(0,0,0,0.2); border: 0.0625rem solid var(--border-color); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${sanitizeId(String(msgId))}', '${safeAttId}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'), '${sanitizeId(msg.key) || "0"}')">
|
||||
<div style="padding: 1rem; text-align: center; color: var(--text-color);">
|
||||
${iconSvg}
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; word-break: break-all;">${escapeHtml(displayName)}</div>
|
||||
|
|
@ -3215,11 +3306,11 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
else if (msg.type === "larp.info.pfp") actionStr = "group.pfp.changed.by";
|
||||
else if (msg.type === "larp.info.pfp_removed") actionStr = "group.pfp.removed.by";
|
||||
|
||||
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
|
||||
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${sanitizeId(msg.author)}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
|
||||
|
||||
let textStr = processBlah(actionStr);
|
||||
if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) {
|
||||
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${sanitizeId(msg.pervious)}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg);
|
||||
} else if (msg.type === "larp.info.history" || msg.type === "larp.info.name") {
|
||||
let escapedContent = escapeHtml(content);
|
||||
|
|
@ -3229,7 +3320,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let targetId = "";
|
||||
let parts = content.split(':');
|
||||
if (parts.length >= 2 && parts[0].includes(';')) {
|
||||
targetId = parts[0];
|
||||
targetId = sanitizeId(parts[0]);
|
||||
targetUsername = escapeHtml(parts.slice(1).join(':'));
|
||||
} else {
|
||||
targetUsername = escapeHtml(parts[0]);
|
||||
|
|
@ -3275,7 +3366,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
dmPfpCache[msg.author] = await getAvatarUrl(msg.author, dmUsernameCache[msg.author]);
|
||||
}
|
||||
if (dmUsernameCache[msg.author]) {
|
||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||
authorName = escapeHtml(dmUsernameCache[msg.author].split(':')[0]);
|
||||
pfp = dmPfpCache[msg.author];
|
||||
}
|
||||
}
|
||||
|
|
@ -3355,7 +3446,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
window.replyAuthorCache[respondedId] = sName;
|
||||
let el = document.getElementById(randId);
|
||||
if (el) {
|
||||
el.innerHTML = `${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${sName}`;
|
||||
el.innerHTML = `${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${escapeHtml(sName)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3365,7 +3456,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
|
||||
let divIdStr = randId !== "" ? `id="${randId}" ` : "";
|
||||
repliedHtml = `<div ${divIdStr}class="chat-message-replied" style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.2rem; cursor: pointer;" onclick="event.stopPropagation(); scrollToMessage('${respondedId}')">${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}</div>`;
|
||||
repliedHtml = `<div ${divIdStr}class="chat-message-replied" style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.2rem; cursor: pointer;" onclick="event.stopPropagation(); scrollToMessage('${sanitizeId(respondedId)}')">${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${escapeHtml(replyAuthor)}</div>`;
|
||||
}
|
||||
|
||||
let reactionsHtml = "";
|
||||
|
|
@ -3405,7 +3496,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let safeRx = encodeURIComponent(rxVal).replace(/'/g, "%27");
|
||||
let escapedRxVal = escapeHtml(rxVal);
|
||||
let pillStyle = `display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.1rem 0.45rem; border-radius: 1rem; font-size: 0.85rem; border: var(--border-width) solid var(--light-border-color); cursor: pointer; ${style}`;
|
||||
reactionsHtml += `<span class="reaction-pill" style="${pillStyle}" onclick="event.stopPropagation(); reactMessagePrompt('${msgId}', '${safeRx}')"><span>${processBlah(escapedRxVal)}</span>${rxCounts[rxVal].count > 1 ? `<span style="opacity: 0.5;">${rxCounts[rxVal].count}</span>` : ""}</span>`;
|
||||
reactionsHtml += `<span class="reaction-pill" style="${pillStyle}" onclick="event.stopPropagation(); reactMessagePrompt('${sanitizeId(String(msgId))}', '${safeRx}')"><span>${processBlah(escapedRxVal)}</span>${rxCounts[rxVal].count > 1 ? `<span style="opacity: 0.5;">${rxCounts[rxVal].count}</span>` : ""}</span>`;
|
||||
}
|
||||
reactionsHtml += `</div>`;
|
||||
}
|
||||
|
|
@ -3415,11 +3506,11 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
|
||||
html += `
|
||||
<div class="chat-message ${extraClass}${isPinnedContext ? ` chat-message-pinned-item` : ``}" data-msg-id="${msgId}" id="${isPinnedContext ? `pinned-` : ``}msg-${msgId}"${isPinnedContext ? ` onclick="scrollToPinnedMessage(event, '${msgId}')"` : ``} style="${isPinnedContext ? `cursor: pointer;` : ``}">
|
||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp" onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer;">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(arguments[0], '${msgId}')">
|
||||
<div class="chat-message ${extraClass}${isPinnedContext ? ` chat-message-pinned-item` : ``}" data-msg-id="${sanitizeId(String(msgId))}" id="${isPinnedContext ? `pinned-` : ``}msg-${sanitizeId(String(msgId))}"${isPinnedContext ? ` onclick="scrollToPinnedMessage(event, '${sanitizeId(String(msgId))}')"` : ``} style="${isPinnedContext ? `cursor: pointer;` : ``}">
|
||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp" onclick="event.stopPropagation(); openProfile('${sanitizeId(msg.author)}')" style="cursor: pointer;">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(arguments[0], '${sanitizeId(String(msgId))}')">
|
||||
${showAvatar ? `<div class="chat-message-header">
|
||||
<span class="chat-message-author" onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer;">${authorName}</span>
|
||||
<span class="chat-message-author" onclick="event.stopPropagation(); openProfile('${sanitizeId(msg.author)}')" style="cursor: pointer;">${authorName}</span>
|
||||
<span class="chat-message-timestamp">${timeStr}</span>
|
||||
</div>` : ""}
|
||||
${repliedHtml}
|
||||
|
|
@ -3498,7 +3589,21 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
let newAttachments = newChild.querySelector('.chat-message-attachments');
|
||||
if (newAttachments) {
|
||||
if (oldAttachments) {
|
||||
if (oldAttachments.getAttribute('data-raw') !== newAttachments.getAttribute('data-raw')) oldAttachments.innerHTML = newAttachments.innerHTML;
|
||||
if (oldAttachments.getAttribute('data-raw') !== newAttachments.getAttribute('data-raw')) {
|
||||
//keep already downloaded/rendered attachment boxes alive across the update,
|
||||
//and always sync data-raw - otherwise every poll would see a stale attribute
|
||||
//and re-render (unloading) the attachments forever
|
||||
let loadedBoxes = {};
|
||||
oldAttachments.querySelectorAll('.attachment-box[data-loaded="true"]').forEach(box => {
|
||||
loadedBoxes[box.id] = box;
|
||||
});
|
||||
oldAttachments.innerHTML = newAttachments.innerHTML;
|
||||
oldAttachments.setAttribute('data-raw', newAttachments.getAttribute('data-raw'));
|
||||
for (let boxId in loadedBoxes) {
|
||||
let freshBox = oldAttachments.querySelector(`#${boxId}`);
|
||||
if (freshBox) freshBox.replaceWith(loadedBoxes[boxId]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let contentNode = existingChild.querySelector('.chat-message-content');
|
||||
if (oldReactions) contentNode.insertBefore(newAttachments, oldReactions);
|
||||
|
|
@ -3756,7 +3861,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
<div class="chat-message" id="${pendingMsgId}" style="opacity: 0.5;">
|
||||
<div style="width: 2.5rem; flex-shrink: 0;"></div>
|
||||
<div class="chat-message-content">
|
||||
<div class="chat-message-text">${content} <span style="font-size: 0.8rem; opacity: 0.7;">(${processBlah("info.sending.message")})</span></div>
|
||||
<div class="chat-message-text">${escapeHtml(content)} <span style="font-size: 0.8rem; opacity: 0.7;">(${processBlah("info.sending.message")})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -3788,6 +3893,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
res = await fetch(`${url}/dm/attachment/staging?id=${id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Crypto": "g1",
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
},
|
||||
body: encryptedFileBytes
|
||||
|
|
@ -3991,6 +4097,10 @@ function setupWebSocket() {
|
|||
}, 300);
|
||||
}
|
||||
}
|
||||
else if (data.startsWith("group_removed:")) {
|
||||
let removedGroupId = data.substring("group_removed:".length);
|
||||
handleGroupRemoved(removedGroupId);
|
||||
}
|
||||
else if (data.startsWith("call_update:")) {
|
||||
if (typeof handleCallUpdate === "function") {
|
||||
handleCallUpdate(data);
|
||||
|
|
@ -4043,7 +4153,7 @@ function handleMessageContextMenu(e, msgId) {
|
|||
if (pinContainer) {
|
||||
let isPinned = msg && msg.pinned === "true";
|
||||
pinContainer.innerHTML = `
|
||||
<button onclick="togglePinMessage('${msgId}')">
|
||||
<button onclick="togglePinMessage('${sanitizeId(String(msgId))}')">
|
||||
${ICONS.pin()}
|
||||
<span class="blah">${isPinned ? processBlah("title.unpin.message") : processBlah("title.pin.message")}</span>
|
||||
</button>`;
|
||||
|
|
@ -4459,7 +4569,14 @@ async function changePassword() {
|
|||
if (!newPass) return;
|
||||
showAction("action.account.saving", "accountsave");
|
||||
|
||||
let newPassHash = await hashSHA3_512(newPass);
|
||||
//sól KDF konta (wymagana; nie zmienia się przy zmianie hasła)
|
||||
let saltRes = await fetchAsync(`${url}/kdfsalt?id=${window.id}`);
|
||||
if (!saltRes || !saltRes.startsWith("P:")) {
|
||||
clearAction("accountsave");
|
||||
showBlahNotification("error:kdf.salt.missing");
|
||||
return;
|
||||
}
|
||||
let newPassHash = await deriveCredentialKdf(newPass, saltRes.substring(2).trim());
|
||||
let newUserKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, newPass);
|
||||
|
||||
let payload = JSON.stringify({
|
||||
|
|
@ -4970,6 +5087,91 @@ if (cropperContainer) {
|
|||
}).observe(cropperContainer);
|
||||
}
|
||||
|
||||
//Usuwa pokój z cache listy (dms/groups). Zwraca true jeśli coś usunięto.
|
||||
function removeRoomFromCachedList(roomId, type) {
|
||||
try {
|
||||
roomId = String(roomId);
|
||||
if (type === 'groups') {
|
||||
if (!window.cachedGroups) return false;
|
||||
let parsed = JSON.parse(window.cachedGroups);
|
||||
let removed = false;
|
||||
if (parsed.groups && parsed.groups[roomId]) {
|
||||
delete parsed.groups[roomId];
|
||||
removed = true;
|
||||
}
|
||||
//fallback: wpis może być zapisany z domena albo bez
|
||||
if (!removed && parsed.groups && !roomId.includes(';')) {
|
||||
for (let key in parsed.groups) {
|
||||
if (key.split(';')[0] === roomId) {
|
||||
delete parsed.groups[key];
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed) window.cachedGroups = JSON.stringify(parsed);
|
||||
return removed;
|
||||
} else {
|
||||
if (!window.cachedDms) return false;
|
||||
let parsed = JSON.parse(window.cachedDms);
|
||||
if (parsed.dms && parsed.dms[roomId]) {
|
||||
delete parsed.dms[roomId];
|
||||
window.cachedDms = JSON.stringify(parsed);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeRoomButton(roomId, type) {
|
||||
let btn = document.getElementById(`${type === 'groups' ? 'group' : 'dm'}-btn-${sanitizeId(roomId)}`);
|
||||
if (btn) btn.remove();
|
||||
}
|
||||
|
||||
//Serwer informuje, że użytkownik został usunięty z grupy (kick) -
|
||||
//usuń grupę z listy, a jeśli jest otwarta, wyjdź z ekranu rozmowy.
|
||||
async function handleGroupRemoved(groupId) {
|
||||
try {
|
||||
removeRoomFromCachedList(groupId, 'groups');
|
||||
removeRoomButton(sanitizeId(groupId), 'groups');
|
||||
if (currentDmId === groupId || (currentDmId && currentDmId.split(';')[0] === groupId.split(';')[0] && window.currentRoomType === 'groups')) {
|
||||
await handleLostRoomAccess(groupId, 'groups', "info:removed.from.group");
|
||||
}
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
//Pokój do którego straciliśmy dostęp: usuń go z listy i wyjdź z ekranu rozmowy.
|
||||
async function handleLostRoomAccess(roomId, type = null, notifyKey = null) {
|
||||
if (!type) type = window.currentRoomType === 'groups' ? 'groups' : 'dms';
|
||||
let wasOpen = currentDmId === roomId;
|
||||
|
||||
removeRoomFromCachedList(roomId, type);
|
||||
removeRoomButton(sanitizeId(roomId), type);
|
||||
|
||||
if (typeof refreshDms === 'function') refreshDms(0, false);
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
|
||||
if (wasOpen) {
|
||||
showBlahNotification(notifyKey || (type === 'groups' ? "error:not.a.member" : "error:forbidden"));
|
||||
if (dmMessagePollInterval) {
|
||||
clearInterval(dmMessagePollInterval);
|
||||
dmMessagePollInterval = null;
|
||||
}
|
||||
currentDmId = null;
|
||||
window.currentRoomType = null;
|
||||
if (mainScreen.classList.contains('mobile-details')) {
|
||||
mainScreen.classList.remove('mobile-details');
|
||||
}
|
||||
gotoHome();
|
||||
}
|
||||
}
|
||||
|
||||
function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
|
||||
try {
|
||||
if (type === 'groups') {
|
||||
|
|
@ -5226,6 +5428,9 @@ function handleMemberContextMenu(e, groupId, memberId) {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
groupId = sanitizeId(groupId);
|
||||
memberId = sanitizeId(memberId);
|
||||
|
||||
if (e && e.type === "contextmenu") {
|
||||
touchContextMenuFired = true;
|
||||
setTimeout(() => { touchContextMenuFired = false; }, 500);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue