LarpixClient/webroot/main.js
olcxja a660ba32bd
All checks were successful
Android Build / publish (push) Successful in 28s
Linux Build / publish (push) Successful in 52s
Switch to X25519 + ML-KEM-768 encryption
2026-05-27 20:40:24 +02:00

1343 lines
No EOL
41 KiB
JavaScript

var prot = window.location.protocol;
async function updateProtocolAndUrl(host)
{
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
if (prot == "http:") {
try {
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
}
catch (error) {
try {
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
prot = "https:";
}
catch (error) {}
}
}
url = `${prot}//${host}/_larpix`;
}
console.log(prot);
var url = `${prot}//${window.location.hostname}/_larpix`;
var params = new URLSearchParams(window.location.search);
try {
var loadingScreen = document.querySelector("loading");
var mainScreen = document.querySelector("main");
var loadingStatus = document.getElementById("loadingstatus");
var addDmBtn = document.getElementById("add-dm-btn");
var addGroupBtn = document.getElementById("add-group-btn");
var sidebarHome = document.getElementById("sidebar-home");
var sidebarHomeButton = sidebarHome.children.item(1);
var sidebarHomeIndicator = sidebarHome.children.item(0);
var sidebarAdd = document.getElementById("sidebar-add");
var sidebarAddButton = sidebarAdd.children.item(1);
var sidebarAddIndicator = sidebarAdd.children.item(0);
var roomsBarContainer = document.getElementById("roomsbar");
var roomDetailsBar = document.getElementById("roomdetailsbar");
var roomContent = document.getElementsByTagName("roomcontent")[0];
var roomsBar = document.getElementsByTagName("roomcontent2")[0];
var sideBar = document.getElementsByTagName("sidebar")[0];
var roomContentMain = document.getElementsByTagName("roomcontent2")[1];
var roomDetailsMain = document.getElementsByTagName("roomcontent2")[2];
var roomContentBar = roomContent.children[0];
var roomsTopBar = document.getElementsByTagName("roomtopbar")[0];
var roomTopBar = document.getElementsByTagName("roomtopbar")[1];
var detailsTopBar = document.getElementsByTagName("roomtopbar")[2];
var sidebarProfile = document.getElementById("sidebar-profile");
var sidebarProfileButton = sidebarProfile.children.item(1);
var sidebarProfileIndicator = sidebarProfile.children.item(0);
var sidebarPfp = sidebarProfileButton.children.item(0);
var sidebarInbox = document.getElementById("sidebar-inbox");
var sidebarInboxButton = sidebarInbox.children.item(1);
var sidebarInboxIndicator = sidebarInbox.children.item(0);
var fixedContextMenu = document.getElementById("fixed-context-menu");
} catch (e) {
}
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function packetEncPass(pass, key, accountId) {
return await encryptWithNonce(pass, key, getNonce(accountId, key));
}
async function getNonce(accountId, key) {
let nonce;
let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`)).text();
try {
nonce = await decryptString(fetchRes, key);
} catch (err) {
nonce = await decryptString(fetchRes, "");
}
return nonce;
}
async function encryptWithNonce(value, key, nonce) {
return await encryptString(value, nonce + key);
}
async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBase64) {
const pubX25519Server = base64ToUint8(pubX25519ServerBase64);
const pubMlKemServer = base64ToUint8(pubMlKemServerBase64);
// X25519
const privX25519Client = window.x25519.utils.randomSecretKey();
const pubX25519Client = window.x25519.getPublicKey(privX25519Client);
const secretX25519 = window.x25519.getSharedSecret(privX25519Client, pubX25519Server);
// ML-KEM-768
const mlkem = new window.MlKem768();
const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemServer);
// Combine and Hash: SHA256(X25519_Secret || MLKEM_Secret)
const combined = new Uint8Array(secretX25519.length + secretMlKem.length);
combined.set(secretX25519);
combined.set(secretMlKem, secretX25519.length);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', combined);
const aesKey = new Uint8Array(hashBuffer);
return [uint8ToBase64(pubX25519Client), uint8ToBase64(ciphertextMlKem), aesKey];
}
function keyDataFromServerJson(jsonFromServer) {
const data = JSON.parse(jsonFromServer);
return [data.pubX25519, data.pubMlKem, data.idKey]
}
function base64ToUint8(base64) {
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
function uint8ToBase64(uint8) {
return fixBase64Padding(btoa(String.fromCharCode(...uint8)));
}
async function encrypt(plainText, keyBytes) {
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const cryptoKey = await window.crypto.subtle.importKey(
"raw", keyBytes, "AES-CBC", false, ["encrypt"]
);
const encryptedContent = await window.crypto.subtle.encrypt(
{name: "AES-CBC", iv: iv},
cryptoKey,
data
);
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
result.set(iv);
result.set(new Uint8Array(encryptedContent), iv.length);
return uint8ToBase64(result);
}
async function decrypt(cipherTextBase64, keyBytes) {
const fullData = base64ToUint8(cipherTextBase64);
const iv = fullData.slice(0, 16);
const cipherData = fullData.slice(16);
const cryptoKey = await window.crypto.subtle.importKey(
"raw", keyBytes, "AES-CBC", false, ["decrypt"]
);
const decrypted = await window.crypto.subtle.decrypt(
{name: "AES-CBC", iv: iv},
cryptoKey,
cipherData
);
return new TextDecoder().decode(decrypted);
}
async function getCryptoKey(passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(passphrase);
const hash = await crypto.subtle.digest('SHA-256', data);
return await crypto.subtle.importKey('raw', hash, {name: 'AES-CBC'}, false, ['encrypt', 'decrypt']);
}
async function encryptBytes(plainBytes, passphrase) {
const key = await getCryptoKey(passphrase);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encryptedContent = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
key,
plainBytes
);
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
result.set(iv);
result.set(new Uint8Array(encryptedContent), iv.length);
return result;
}
async function decryptBytes(combinedBytes, passphrase) {
const key = await getCryptoKey(passphrase);
const iv = combinedBytes.slice(0, 16);
const data = combinedBytes.slice(16);
const decryptedContent = await crypto.subtle.decrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
return new Uint8Array(decryptedContent);
}
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']
);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encrypted = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return fixBase64Padding(btoa(String.fromCharCode(...combined)));
}
async function decryptString(base64Text, passphrase) {
const encoder = new TextEncoder();
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
const key = await crypto.subtle.importKey(
'raw', pwHash, {name: 'AES-CBC'}, false, ['decrypt']
);
const iv = combined.slice(0, 16);
const data = combined.slice(16);
const decrypted = await crypto.subtle.decrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
return new TextDecoder().decode(decrypted);
}
async function fetchPost(url, value) {
let response = await fetch(url, {
method: "POST",
body: value,
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
return data;
}
async function fetchPostEnc(url, value) {
let nonce = await getNonce(id, passwordHash);
let response = await fetch(url, {
method: "POST",
body: await encryptWithNonce(value, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return data;
}
async function fetchAsync(url) {
let response = await fetch(url, {
method: "GET",
});
let data = await response.text();
return data;
}
async function fetchAsyncWAuth(url) {
let response = await fetch(url, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
return data;
}
async function getServerInfo(host) {
console.log(`${prot}//${host}/_larpix/serverinfo`)
return JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
}
async function Auth(loginUsername, loginPassword) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`);
if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) {
return "error:invalid.username.or.password";
}
let actualId = resolvedId.split(":")[0];
let passwordHash = await hashSHA3_512(loginPassword);
let response = await fetch(`${url}/auth?id=${actualId}`, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
}
});
let data = await response.text();
if (data.startsWith("success:")) {
data += "|" + actualId;
}
return data;
}
async function fetchEncrypted(request, body = "") {
let nonce = await getNonce(id, passwordHash);
let response = await fetch(`${url}/encryptedrequest?id=${id}`, {
method: "POST",
body: await encryptWithNonce(
JSON.stringify({
string1: request,
string2: body
})
, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return decryptString(data, passwordHash);
}
function power(base, exponent, mod) {
let res = 1n;
base = base % mod;
while (exponent > 0n) {
if (exponent % 2n === 1n) res = (res * base) % mod;
base = (base * base) % mod;
exponent = exponent / 2n;
}
return res;
}
function fixBase64Padding(base64String) {
let str = base64String.replace(/-/g, '+').replace(/_/g, '/');
const pad = str.length % 4;
if (pad) {
if (pad === 1) {
throw new Error("");
}
str += '='.repeat(4 - pad);
}
return str;
}
function showBlahNotification(blahmessage, duration = 3500) {
try {
let split = blahmessage.split(":");
showNotification(processBlah(blahmessage), split[0], duration);
} catch (e) {
showNotification(blahmessage, "info", duration);
}
}
function showNotification(message, type = 'info', duration = 3500) {
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
}
const notif = document.createElement('div');
notif.className = `notification ${type}`;
notif.textContent = message;
container.appendChild(notif);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
notif.classList.add('show');
});
});
setTimeout(() => {
notif.classList.remove('show');
notif.addEventListener('transitionend', () => {
notif.remove();
});
setTimeout(() => notif.remove(), 300);
}, duration);
}
function showAction(message, actionid) {
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
}
const notif = document.createElement('div');
notif.className = `notification`;
notif.textContent = processBlah(message);
notif.id = `notification-${actionid}`;
container.appendChild(notif);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
notif.classList.add('show');
});
});
}
function clearAction(actionid) {
const notifications = document.querySelectorAll(`[id="notification-${actionid}"]`);
notifications.forEach(notif => {
notif.classList.remove('show');
notif.addEventListener('transitionend', async () => {
notif.remove();
}, { once: true });
setTimeout(() => notif.remove(), 300);
});
}
async function hashSHA3_512(input) {
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;
}
//placeholder pfp
function getInitials(name) {
const cleanName = (name || "").trim();
//name empty
if (!cleanName) return "";
const parts = cleanName.split(/\s+/);
//at least 2 words
if (parts.length >= 2) {
const firstInitial = parts[0][0];
const secondInitial = parts[parts.length - 1][0]; //from last word
return (firstInitial + secondInitial).toUpperCase();
}
//only 1 word with 1 letter
const word = parts[0];
if (word.length === 1) {
return word.toUpperCase();
}
//1 word at least 2 letters
return word.substring(0, 2).toUpperCase();
}
function stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const h = Math.abs(hash) % 360;
const s = 50;
const l = 65;
return `hsl(${h}, ${s}%, ${l}%)`;
}
function createAvatarSvg(name, size = 512) {
const initials = getInitials(name);
const color = stringToColor(name);
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="100%" height="100%" fill="${color}" />
<text
x="50%"
y="50%"
fill="white"
font-family="Nunito"
font-size="${size * 0.45}"
text-anchor="middle"
dy=".35em">
${initials}
</text>
</svg>
`.trim();
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
async function getAvatarUrl(id, username)
{
try {
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
if ((await fetchAsync(pfpUrl)) == "")
{
throw Error();
}
return pfpUrl;
}
catch (e) {
return createAvatarSvg(username);
}
}
function updateLoadingStatus(message) {
loadingStatus.innerHTML = processBlah(message);
}
async function loadingFadeOut() {
mainScreen.style.transform = "scale(0.85)";
mainScreen.style.opacity = "0";
loadingScreen.style.transform = "scale(0.85)";
loadingScreen.style.opacity = "0";
await delay(200);
loadingScreen.style.display = "none";
mainScreen.style.display = "";
await delay(200);
mainScreen.style.transform = "";
mainScreen.style.opacity = "";
}
async function loadingFadeIn() {
loadingScreen.style.transform = "scale(0.85)";
loadingScreen.style.opacity = "0";
mainScreen.style.transform = "scale(0.85)";
mainScreen.style.opacity = "0";
await delay(200);
mainScreen.style.display = "none";
loadingScreen.style.display = "";
await delay(200);
loadingScreen.style.transform = "";
loadingScreen.style.opacity = "";
}
var blah;
async function initBlahs() {
lang = lang.toLowerCase();
let res;
let path = window.location.pathname.replace("/login/index.html", "/");
try { //try user lang first
res = await fetchAsync(`${path}blah/${lang}.json`);
} catch (e) { //fallback to en-us
res = await fetchAsync(`${path}blah/en-us.json`);
}
blah = JSON.parse(res);
let blahTags = document.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = document.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = document.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
}
function splitLimit(str, separator, limit) {
const parts = str.split(separator);
if (parts.length <= limit) {
return parts;
}
return [
...parts.slice(0, limit - 1),
parts.slice(limit - 1).join(separator)
];
}
function processBlah(blahmessage) {
try {
if (!blahmessage.includes(":")) {
blahmessage = `:${blahmessage}`;
}
let split = splitLimit(blahmessage, ":", 3);
let values = [];
try {
values = split[2].split(";");
} catch (e) {
}
let message = blah[split[1]];
let valueslist = "";
for (let i = 0; i < values.length; i++) {
let value = processBlah(values[i]);
if (value.startsWith(":"))
{
value = value.substring(1);
}
valueslist+=`${value}, `;
message = message.replaceAll(`{${i}}`, value);
}
valueslist = valueslist.slice(0, -2);
if (message.includes('{all}')) {
message = message.replaceAll('{all}', valueslist);
}
return message;
}
catch (e)
{
return blahmessage;
}
}
function getLang() {
return (navigator.language || navigator.languages[0]);
}
var id = "";
var password = "";
var username = "";
var passwordHash = "";
var host = "";
var lang = getLang();
async function mainJS() {
await initBlahs();
passwordHash = await hashSHA3_512(password);
if (localStorage.getItem('lang') != null) {
lang = localStorage.getItem('lang');
}
if (host) {
await updateProtocolAndUrl(host);
} else {
await updateProtocolAndUrl(window.location.hostname);
}
if (!id && username) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${username}`);
if (resolvedId && !resolvedId.startsWith("error:")) {
id = resolvedId.split(":")[0];
localStorage.setItem("id", id);
}
}
await start();
}
id = localStorage.getItem('id');
username = localStorage.getItem('username');
password = localStorage.getItem('password');
host = localStorage.getItem('host');
mainJS();
function showFixedContextMenu(rect, html) {
let parser = new DOMParser();
let doc = parser.parseFromString(html, "text/html");
let blahTags = doc.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = doc.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = doc.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
fixedContextMenu.innerHTML = doc.body.innerHTML;
fixedContextMenu.style.left = `${rect.right + 10}px`;
fixedContextMenu.style.top = `${rect.top}px`;
fixedContextMenu.classList.add("show");
}
var currentRoomsBarTitle = null;
var currentRoomsBarHtml = null;
async function switchRoomsBar(title, content) {
if (currentRoomsBarTitle === title && currentRoomsBarHtml === content) return;
currentRoomsBarTitle = title;
currentRoomsBarHtml = content;
let roomsTopBarTransition = roomsTopBar.children.item(0);
roomsBar.style.transform = "scale(0.85)";
roomsBar.style.opacity = "0";
roomsTopBarTransition.style.transform = "scale(0.85)";
roomsTopBarTransition.style.opacity = "0";
await delay(200);
roomsTopBarTransition.innerHTML = processBlah(title);
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
let blahTags = doc.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = doc.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = doc.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
roomsBar.innerHTML = doc.body.innerHTML;
roomsBar.style.transform = "";
roomsBar.style.opacity = "";
roomsTopBarTransition.style.transform = "";
roomsTopBarTransition.style.opacity = "";
}
var currentRoomContentTitle = null;
var currentRoomContentHtml = null;
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false)
{
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
if (title != "title.splash") { //do not show splash on mobile
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
}
}
return;
}
currentRoomContentTitle = title;
currentRoomContentHtml = content;
let roomsTopBarTransition = roomTopBar.children.item(0);
roomContentMain.style.transform = "scale(0.85)";
roomContentMain.style.opacity = "0";
roomsTopBarTransition.style.transform = "scale(0.85)";
roomsTopBarTransition.style.opacity = "0";
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
if (title != "title.splash") //do not show splash on mobile
{
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
}
}
else
{
await delay(200);
}
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}<space></space><inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${detailsBtnHtml}`;
roomDetailsBar.style.display = showRoomBar ? "flex" : "none";
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
let blahTags = doc.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = doc.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = doc.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
roomContentMain.innerHTML = doc.body.innerHTML;
roomContentMain.style.transform = "";
roomContentMain.style.opacity = "";
roomsTopBarTransition.style.transform = "";
roomsTopBarTransition.style.opacity = "";
}
var currentDetailsContentTitle = null;
var currentDetailsContentHtml = null;
async function switchDetailsContent(title, content)
{
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
currentDetailsContentTitle = title;
currentDetailsContentHtml = content;
let roomsTopBarTransition = detailsTopBar.children.item(0);
roomDetailsMain.style.transform = "scale(0.85)";
roomDetailsMain.style.opacity = "0";
roomsTopBarTransition.style.transform = "scale(0.85)";
roomsTopBarTransition.style.opacity = "0";
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth > 52 * rem) {
await delay(200);
}
roomsTopBarTransition.innerHTML = processBlah(title);
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
let blahTags = doc.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = doc.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = doc.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
roomDetailsMain.innerHTML = doc.body.innerHTML;
roomDetailsMain.style.transform = "";
roomDetailsMain.style.opacity = "";
roomsTopBarTransition.style.transform = "";
roomsTopBarTransition.style.opacity = "";
}
function clickCollapseDms()
{
var collapseDmsBtn = document.getElementById("collapse-dms");
collapseDmsBtn.classList.toggle("collapsed");
}
function clickCollapseGroups()
{
var collapseGroupsBtn = document.getElementById("collapse-groups");
collapseGroupsBtn.classList.toggle("collapsed");
}
function clickAddGroup()
{
switchRoomContent("title.create.group", createGroupScreen, false);
}
function clickAddDm()
{
switchRoomContent("title.create.dm", addDmScreen, false);
}
var currentMainIndicator = null;
sidebarAddButton.addEventListener("click", (e) => {
e.stopPropagation();
history.pushState({trap: true}, "", location.href);
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
setActiveSidebarIndicator(currentMainIndicator);
} else {
setActiveSidebarIndicator(sidebarAddIndicator, true);
const rect = sidebarAddButton.getBoundingClientRect();
showFixedContextMenu(rect, addSpaceMenu);
}
});
document.addEventListener("click", (e) => {
history.pushState({trap: true}, "", location.href);
if (fixedContextMenu.classList.contains("show")) {
if (!fixedContextMenu.contains(e.target)) {
fixedContextMenu.classList.remove("show");
if (!e.target.closest('sidebarelement')) {
setActiveSidebarIndicator(currentMainIndicator);
}
}
}
if (mainScreen && mainScreen.classList.contains('mobile-details')) {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth <= 52 * rem) {
if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right')) {
mobileNavBack();
}
}
}
});
window.addEventListener("popstate", (e) => {
popstate();
});
const { App } = window.Capacitor?.Plugins || {};
if (App) {
App.addListener('backButton', () => {
popstate();
});
}
function popstate()
{
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details'))
{
history.back();
if (App) {
App.minimizeApp();
}
}
mobileNavBack();
}
function gotoSideProfilePopup() {
}
function setActiveRoombarItem(itemId) {
let items = document.querySelectorAll('.collapse-text-button');
items.forEach(item => item.classList.remove('selected'));
if (itemId) {
let activeItem = document.getElementById(itemId);
if (activeItem) activeItem.classList.add('selected');
}
}
async function renderInvites(res, type) {
let container = document.getElementById("invites-container");
if (!container) return;
if (res && res != "") {
res = JSON.parse(res);
let invites = [
...Object.entries(res.dms).map(([id, value]) => ({
id,
type: "dm",
...value
})),
...Object.entries(res.groups).map(([id, value]) => ({
id,
type: "group",
...value
}))
].sort((a, b) => b.timestamp - a.timestamp);
container.classList.remove("empty");
let html = "";
let count = 0;
for (let i = 0; i < invites.length; i++) {
let invite = invites[i];
if (invite.type === "dm") {
let id = invite.id;
if (!id.includes(":")) {
id += `:${host}`;
}
let username = await fetchAsync(`${url}/idtoname?id=${id}`);
if (!(username && username.trim() !== "")) return;
count++;
let pfp = await getAvatarUrl(id, username);
let actions = "";
let desc = "";
if (type === "received") {
desc = `:desc.invite.dm.received:${username};${id}`;
actions = `
<button class="icon-button" style="background-color: var(--big-green);" onclick="acceptInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>
</button>
<button class="icon-button" style="background-color: var(--big-red);" onclick="declineInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</button>
`;
} else {
desc = `:desc.invite.dm.sent:${username};${id}`;
actions = `
<button class="icon-button" style="background-color: var(--big-red);" onclick="revokeInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</button>
`;
}
let entry = invitesEntry
.replaceAll("{username}", username.split(':')[0])
.replaceAll("{pfp}", pfp)
.replaceAll("{desc}", desc)
.replaceAll("{actions}", actions);
html += entry;
}
}
if (count > 0) {
container.innerHTML = html;
} else {
container.classList.add("empty");
container.innerHTML = invitesEmptyState;
}
} else {
container.classList.add("empty");
container.innerHTML = invitesEmptyState;
}
let blahTags = container.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
async function loadInvites(tab) {
try {
showAction(`action.fetching.invites.${tab === 'received' ? 'recv' : 'sent'}`, `fetching.invites.${tab}`);
let res = await fetchEncrypted(`user/invites/${tab}`);
await renderInvites(res, tab);
} catch (e) {
showBlahNotification("error:something.wrong");
console.error(e);
await renderInvites("", tab);
}
clearAction(`fetching.invites.${tab}`);
}
async function switchInvitesTab(tab) {
if (!document.getElementById(`tab-invites-${tab}`).classList.contains('tab-inactive')) return;
document.getElementById('tab-invites-received').classList.add('tab-inactive');
document.getElementById('tab-invites-sent').classList.add('tab-inactive');
document.getElementById(`tab-invites-${tab}`).classList.remove('tab-inactive');
await loadInvites(tab);
}
async function acceptInvite(targetId) { //TODO: Implement key generation
try {
showAction("action.invite.accepting", "invite.action");
let payload = JSON.stringify({
string1: targetId,
string2: "", // TODO: Generate symmetric keys
string3: "" // TODO: Encrypt key for targetId
});
let res = await fetchEncrypted("user/dm/create", payload);
clearAction("invite.action");
showBlahNotification(res || "success:invite.accepted");
await loadInvites('received');
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
}
}
async function declineInvite(username) {
try {
showAction("action.invite.declining", "invite.action");
let res = await fetchEncrypted("user/dm/invite/decline", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.declined");
await loadInvites('received');
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
}
}
async function revokeInvite(username) {
try {
showAction("action.invite.revoking", "invite.action");
let res = await fetchEncrypted("user/dm/invite/revoke", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.revoked");
await loadInvites('sent');
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
}
}
function switchNotifisTab(tab) {
if (!document.getElementById(`tab-notifis-${tab}`).classList.contains('tab-inactive')) return;
document.getElementById('tab-notifis-all').classList.add('tab-inactive');
document.getElementById('tab-notifis-unread').classList.add('tab-inactive');
document.getElementById(`tab-notifis-${tab}`).classList.remove('tab-inactive');
}
async function gotoInbox() {
if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar
setActiveSidebarIndicator(sidebarInboxIndicator);
await switchRoomsBar("title.inbox", inboxRoomBar);
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth > 52 * rem) {
gotoInvites();
}
}
async function gotoInvites() {
setActiveRoombarItem('collapse-invites');
await switchRoomContent("title.invites", invitesScreen, false);
switchInvitesTab('received');
}
async function gotoNotifis() {
setActiveRoombarItem('collapse-notifis');
await switchRoomContent("title.notifications", notifisScreen, false);
switchNotifisTab('all');
}
function gotoCreateSpace() {
fixedContextMenu.classList.remove("show");
if (roomsBarContainer) roomsBarContainer.style.display = "none"; //hide roombar
switchRoomContent("title.create.space", createSpaceScreen, false);
}
function gotoJoinSpace() {
fixedContextMenu.classList.remove("show");
if (roomsBarContainer) roomsBarContainer.style.display = "none"; //hide roombar
switchRoomContent("title.join.space", joinSpaceScreen, false);
}
function gotoHome() {
if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar
setActiveSidebarIndicator(sidebarHomeIndicator);
switchRoomContent("title.splash", splashScreen, false);
switchRoomsBar("title.home", homeRoomBar);
setActiveRoombarItem(null);
}
function setActiveSidebarIndicator(element, isTemporary = false) {
let indicators = document.getElementsByTagName("indicator");
for (let i = 0; i < indicators.length; i++) {
indicators[i].classList.remove("active");
}
if (element) {
element.classList.add("active");
}
if (!isTemporary && element) {
currentMainIndicator = element;
}
}
function mobileNavBack() {
if(mainScreen.classList.contains('mobile-details')) {
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
setActiveSidebarIndicator(currentMainIndicator);
} else {
setActiveRoombarItem(null);
if (roomsBarContainer) {
roomsBarContainer.style.display = ""; //restore roombar on mobile
void roomsBarContainer.offsetWidth;
}
mainScreen.classList.remove('mobile-content');
setActiveSidebarIndicator(currentMainIndicator);
}
}
function mobileNavDetails() {
mainScreen.classList.add('mobile-details');
}
let touchStartX = 0;
let touchEndX = 0;
let touchStartY = 0;
let touchEndY = 0;
let touchMoved = false;
document.addEventListener('touchstart', e => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
touchMoved = false;
activeTouchButton = e.target.closest('button, input');
if (activeTouchButton) {
activeTouchButton.classList.add('is-pressed');
}
}, { passive: true });
document.addEventListener('touchmove', e => {
if (!touchMoved) {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
let diffX = Math.abs(e.changedTouches[0].screenX - touchStartX);
let diffY = Math.abs(e.changedTouches[0].screenY - touchStartY);
if (diffX > rem || diffY > rem) {
touchMoved = true;
if (activeTouchButton) {
activeTouchButton.classList.remove('is-pressed');
activeTouchButton = null;
}
}
}
}, { passive: true });
document.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY;
handleMobileSwipe();
if (activeTouchButton) {
activeTouchButton.classList.remove('is-pressed');
if (!touchMoved) {
if (activeTouchButton.tagName !== 'INPUT') {
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
document.activeElement.blur();
}
if (e.cancelable) {
e.preventDefault();
}
activeTouchButton.click();
} else {
activeTouchButton.focus();
}
}
activeTouchButton = null;
}
});
document.addEventListener('touchcancel', () => {
if (activeTouchButton) {
activeTouchButton.classList.remove('is-pressed');
activeTouchButton = null;
}
});
document.addEventListener('contextmenu', e => {
if (e.target.closest('button')) {
e.preventDefault();
}
});
function handleMobileSwipe() {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth > 52 * rem) return;
let diffX = touchEndX - touchStartX;
let diffY = touchEndY - touchStartY;
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 4 * rem) {
if (diffX > 0) {
mobileNavBack();
} else {
if (mainScreen.classList.contains('mobile-content') && roomDetailsBar.style.display !== 'none') {
mobileNavDetails();
}
}
}
}