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) {
    let binary = '';
    const chunkSize = 8192;
    for (let i = 0; i < uint8.length; i += chunkSize) {
        binary += String.fromCharCode.apply(null, uint8.subarray(i, i + chunkSize));
    }
    return fixBase64Padding(btoa(binary));
}

function concatUint8(a, b) {
    const out = new Uint8Array(a.length + b.length);
    out.set(a, 0);
    out.set(b, a.length);
    return out;
}

async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) {
    const enc = new TextEncoder();
    const keyMaterial = await crypto.subtle.importKey(
        "raw",
        enc.encode(passwordPlain),
        {name: "PBKDF2"},
        false,
        ["deriveKey"]
    );
    return crypto.subtle.deriveKey(
        {
            name: "PBKDF2",
            salt,
            iterations,
            hash: "SHA-256"
        },
        keyMaterial,
        {name: "AES-GCM", length: 256},
        false,
        ["encrypt", "decrypt"]
    );
}

async function encryptJsonWithPassword(jsonObj, passwordPlain) {
    const salt = crypto.getRandomValues(new Uint8Array(16));
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const iterations = 310000;
    const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations);
    const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj));
    const ciphertextBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, plaintext);
    const ciphertext = new Uint8Array(ciphertextBuf);

    return JSON.stringify({
        v: 1,
        kdf: "PBKDF2-SHA256",
        iter: iterations,
        alg: "AES-256-GCM",
        salt: uint8ToBase64(salt),
        iv: uint8ToBase64(iv),
        ct: uint8ToBase64(ciphertext)
    });
}

async function decryptJsonWithPassword(envelopeJson, passwordPlain) {
    const env = JSON.parse(envelopeJson);
    if (!env || env.v !== 1 || env.kdf !== "PBKDF2-SHA256" || env.alg !== "AES-256-GCM") {
        throw new Error("unsupported.key.envelope");
    }
    const salt = base64ToUint8(env.salt);
    const iv = base64ToUint8(env.iv);
    const ct = base64ToUint8(env.ct);
    const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter);
    const plainBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
    return JSON.parse(new TextDecoder().decode(plainBuf));
}

async function sha256Bytes(bytes) {
    const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
    return new Uint8Array(hashBuffer);
}

async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length = 32) {
    const info = new TextEncoder().encode(infoString);
    const salt = saltBytes ?? new Uint8Array(32);
    const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]);
    const bits = await crypto.subtle.deriveBits(
        {name: "HKDF", hash: "SHA-256", salt, info},
        keyMaterial,
        length * 8
    );
    return new Uint8Array(bits);
}

async function importAesGcmKey(keyBytes) {
    return crypto.subtle.importKey("raw", keyBytes, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]);
}

async function encryptAesGcmToBase64(plainText, keyBytes) {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const key = await importAesGcmKey(keyBytes);
    const pt = new TextEncoder().encode(plainText);
    const ctBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, pt);
    const combined = new Uint8Array(iv.length + ctBuf.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(ctBuf), iv.length);
    return uint8ToBase64(combined);
}

async function decryptAesGcmFromBase64(cipherBase64, keyBytes) {
    const combined = base64ToUint8(cipherBase64);
    const iv = combined.slice(0, 12);
    const ct = combined.slice(12);
    const key = await importAesGcmKey(keyBytes);
    const ptBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
    return new TextDecoder().decode(ptBuf);
}

async function encryptAesGcmToBytes(plainBytes, keyBytes) {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const key = await importAesGcmKey(keyBytes);
    const ctBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, plainBytes);
    const combined = new Uint8Array(iv.length + ctBuf.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(ctBuf), iv.length);
    return combined;
}

async function decryptAesGcmFromBytes(cipherBytes, keyBytes) {
    const iv = cipherBytes.slice(0, 12);
    const ct = cipherBytes.slice(12);
    const key = await importAesGcmKey(keyBytes);
    const ptBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
    return new Uint8Array(ptBuf);
}

function randomRoomKey256Chars() {
    const bytes = crypto.getRandomValues(new Uint8Array(192));
    return uint8ToBase64(bytes);
}

async function deriveDmSelfWrapKeyBytes() {
    if (!userKeysPrivate) throw new Error("missing.user.private.keys");
    const privX = base64ToUint8(userKeysPrivate.privX25519);
    const privM = base64ToUint8(userKeysPrivate.privMlKem);
    return await hkdfSha256Bytes(concatUint8(privX, privM), "larpix:dm:selfwrap:v1");
}

async function wrapRoomKeyForSelf(roomKey) {
    const keyBytes = await deriveDmSelfWrapKeyBytes();
    const enc = await encryptAesGcmToBase64(roomKey, keyBytes);
    return `DMK1:${enc}`;
}

async function unwrapRoomKeyForSelf(wrapped) {
    if (!wrapped || !wrapped.startsWith("DMK1:")) throw new Error("unsupported.dm.key.format");
    const keyBytes = await deriveDmSelfWrapKeyBytes();
    return await decryptAesGcmFromBase64(wrapped.substring("DMK1:".length), keyBytes);
}

async function deriveHybridKeyToUser(targetPublicKeysObj) {
    const pubX25519Target = base64ToUint8(targetPublicKeysObj.pubX25519);
    const pubMlKemTarget = base64ToUint8(targetPublicKeysObj.pubMlKem);

    const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519);
    const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Target);

    const mlkem = new window.MlKem768();
    const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemTarget);

    const combined = concatUint8(secretX25519, secretMlKem);
    const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1");
    return {
        aesKeyBytes,
        ciphertextMlKemBase64: uint8ToBase64(ciphertextMlKem)
    };
}

async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase64) {
    const pubX25519Other = base64ToUint8(otherPublicKeysObj.pubX25519);
    const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519);
    const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Other);

    const ct = base64ToUint8(ciphertextMlKemBase64);
    const privMlKemMine = base64ToUint8(userKeysPrivate.privMlKem);
    const mlkem = new window.MlKem768();
    const secretMlKem = await mlkem.decap(ct, privMlKemMine);

    const combined = concatUint8(secretX25519, secretMlKem);
    const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1");
    return aesKeyBytes;
}

function userPublicKeysFingerprint(pub) {
    if (!pub) return "";
    return `${pub.pubX25519 || ""}|${pub.pubMlKem || ""}`;
}

async function fetchDmKey(dmId) {
    return await fetchEncrypted("dm/key/get", dmId);
}

async function updateDmKey(dmId, keyValue) {
    return await fetchEncrypted(
        "dm/key/update",
        JSON.stringify({
            string1: dmId,
            string2: keyValue
        })
    );
}

async function ensureDmRoomKey(dmId) {
    //returns decrypted roomkey, and converts SETUP key
    await ensureUserKeys();
    const raw = await fetchDmKey(dmId);
    if (!raw) throw new Error("error:dm.key.missing");

    if (raw.startsWith("SETUP:")) {
        const rest = raw.substring("SETUP:".length);
        const parts = rest.split(";");
        if (parts.length < 2) throw new Error("error:dm.key.setup.malformed");

        const otherPublicJson = parts[0];
        const setupPayloadJson = parts.slice(1).join(";");
        const otherPublic = JSON.parse(otherPublicJson);
        const setupPayload = JSON.parse(setupPayloadJson);

        const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem);
        const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);

        const selfWrapped = await wrapRoomKeyForSelf(roomKey);
        await unwrapRoomKeyForSelf(selfWrapped);
        await updateDmKey(dmId, selfWrapped);
        return roomKey;
    }

    return await unwrapRoomKeyForSelf(raw);
}

window.ensureDmRoomKey = ensureDmRoomKey;

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 uint8ToBase64(combined);
}

async function encryptBytesWithNonce(dataUint8, key, nonce) {
    const encoder = new TextEncoder();
    const passphrase = nonce + key;
    const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));

    const importedKey = 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},
        importedKey,
        dataUint8
    );

    const combined = new Uint8Array(iv.length + encrypted.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encrypted), iv.length);

    return 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);
}


let requestMutex = Promise.resolve();

async function fetchPost(url, value) {
    let release;
    const lock = new Promise(resolve => release = resolve);
    const prevMutex = requestMutex;
    requestMutex = prevMutex.then(() => lock);
    await prevMutex;
    try {
        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;
    } finally {
        release();
    }
}

async function fetchPostEnc(url, value) {
    let release;
    const lock = new Promise(resolve => release = resolve);
    const prevMutex = requestMutex;
    requestMutex = prevMutex.then(() => lock);
    await prevMutex;
    try {
        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;
    } finally {
        release();
    }
}

async function fetchAsync(url) {
    let response = await fetch(url, {
        method: "GET",
    });

    let data = await response.text();
    return data;
}

async function fetchAsyncWAuth(url) {
    let release;
    const lock = new Promise(resolve => release = resolve);
    const prevMutex = requestMutex;
    requestMutex = prevMutex.then(() => lock);
    await prevMutex;
    try {
        let response = await fetch(url, {
            method: "GET",
            headers: {
                "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
            }
        });

        let data = await response.text();
        return data;
    } finally {
        release();
    }
}

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 release;
    const lock = new Promise(resolve => release = resolve);
    const prevMutex = requestMutex;
    requestMutex = prevMutex.then(() => lock);
    await prevMutex;
    try {
        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;
    } finally {
        release();
    }
}


async function fetchEncrypted(request, body = "") {
    let release;
    const lock = new Promise(resolve => release = resolve);
    const prevMutex = requestMutex;
    requestMutex = prevMutex.then(() => lock);
    await prevMutex;
    try {
        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();
        if (data.startsWith("error:")) return data;
        return await decryptString(data, passwordHash);
    } catch (e) {
        console.error("fetchEncrypted error:", e);
        return "error:network.error";
    } finally {
        release();
    }
}

async function fetchEncryptedUpload(target, fileBytesUint8) {
    let release;
    const lock = new Promise(resolve => release = resolve);
    const prevMutex = requestMutex;
    requestMutex = prevMutex.then(() => lock);
    await prevMutex;
    try {
        let nonce = await getNonce(id, passwordHash);

        let response = await fetch(`${url}/upload?id=${id}`, {
            method: "POST",
            body: await encryptBytesWithNonce(fileBytesUint8, passwordHash, nonce),
            headers: {
                "secret": await encryptWithNonce(passwordHash, passwordHash, nonce),
                "target": await encryptWithNonce(target, passwordHash, nonce)
            }
        });
        let data = await response.text();
        if (data.startsWith("error:")) return data;
        return await decryptString(data, passwordHash);
    } catch (e) {
        console.error("fetchEncryptedUpload error:", e);
        return "error:network.error";
    } finally {
        release();
    }
}


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 = `
        ${ICONS.avatar(initials, color, size)}
    `.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 (id === window.id && window.profileUpdateTimestamp) pfpUrl += `&t=${window.profileUpdateTimestamp}`;
        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`);
        blah = JSON.parse(res);
    } 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) {
    let prepended = false;
    try {
        if (!blahmessage.includes(":")) {
            blahmessage = `:${blahmessage}`;
            prepended = true;
        }

        let split = splitLimit(blahmessage, ":", 3);

        let values = [];
        try {
            if (split[2]) {
                values = split[2].split(";");
            }
        } catch (e) {
        }

        let message = blah[split[1]];
        if (message === undefined) throw new Error();

        let valueslist = "";
        for (let i = 0; i < values.length; i++) {
            let value = processBlah(values[i]);
            valueslist += `${value}, `;
            message = message.replaceAll(`{${i}}`, value);
        }
        if (values.length > 0) {
            valueslist = valueslist.slice(0, -2);
        }
        if (message.includes('{all}')) {
            message = message.replaceAll('{all}', valueslist);
        }
        return message;
    } catch (e) {
        if (blahmessage.startsWith("error:")) {
            return blah["error.unknown.error"] || "Unknown error";
        }
        if (prepended) {
            return blahmessage.substring(1);
        }
        return blahmessage;
    }
}

function getLang() {
    return (navigator.language || navigator.languages[0]);
    //return "en-cat";
}

var id = "";
var password = "";
var username = "";
var passwordHash = "";
var host = "";
var lang = getLang();


var userKeysPublic = null;   //2string json
var userKeysPrivate = null;  // decrypted private bundle (JSON object)
var userKeysEncrypted = null; // encrypted private bundle (string envelope JSON)

function publishUserKeysGlobals() {
    window.userKeysPublic = userKeysPublic;
    window.userKeysPrivate = userKeysPrivate;
    window.userKeysEncrypted = userKeysEncrypted;
}

async function generateUserKeysBundle() {
    // X25519 keypair
    const privX25519 = window.x25519.utils.randomSecretKey();
    const pubX25519 = window.x25519.getPublicKey(privX25519);

    // ML-KEM-768 keypair
    const mlkem = new window.MlKem768();
    const [pubMlKem, privMlKem] = await mlkem.generateKeyPair();

    const pub = {
        v: 1,
        alg: "hybrid-x25519-mlkem768",
        pubX25519: uint8ToBase64(pubX25519),
        pubMlKem: uint8ToBase64(pubMlKem)
    };
    const priv = {
        v: 1,
        alg: "hybrid-x25519-mlkem768",
        privX25519: uint8ToBase64(privX25519),
        privMlKem: uint8ToBase64(privMlKem)
    };
    return {pub, priv};
}

async function ensureUserKeys() {
    const lsEncKey = `userKeys.enc.v1:${id}`;
    const lsPubKey = `userKeys.pub.v1:${id}`;

    let serverKeys = null;
    let serverKeysRaw = "";
    try { //1. check server
        serverKeysRaw = await fetchEncrypted("user/key/get");
        if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) {
            serverKeys = JSON.parse(serverKeysRaw);
            const serverEncPriv = serverKeys.string1;
            const serverPub = serverKeys.string2;
            if (serverEncPriv && serverPub) {
                userKeysEncrypted = serverEncPriv;
                userKeysPublic = JSON.parse(serverPub);
                userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
                localStorage.setItem(lsEncKey, userKeysEncrypted);
                localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic));
                publishUserKeysGlobals();
                return "success:keys.server";
            }
        }
    } catch (e) {
        if (serverKeys?.string1) {
            throw new Error("error:keys.server.decrypt.failed");
        }
    }

    //fallback to local
    const localEnc = localStorage.getItem(lsEncKey);
    const localPub = localStorage.getItem(lsPubKey);
    if (localEnc && localPub) {
        try {
            const localPubObj = JSON.parse(localPub);
            if (serverKeys?.string2) {
                const serverPubObj = JSON.parse(serverKeys.string2);
                if (userPublicKeysFingerprint(localPubObj) !== userPublicKeysFingerprint(serverPubObj)) {
                    throw new Error("error:keys.local.server.mismatch");
                }
            }

            userKeysEncrypted = localEnc;
            userKeysPublic = localPubObj;
            userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);

            //push keys to server only when server has none yet
            if (!serverKeys?.string1) {
                const updateRes = await fetchEncrypted(
                    "user/key/update",
                    JSON.stringify({
                        string1: userKeysEncrypted,
                        string2: JSON.stringify(userKeysPublic)
                    })
                );
                if (updateRes === "error:keys.public.mismatch") {
                    throw new Error("error:keys.local.server.mismatch");
                }
            }

            publishUserKeysGlobals();
            return "success:keys.local";
        } catch (e) {
            if (e.message && e.message.startsWith("error:keys.")) {
                throw e;
            }
        }
    }

    if (serverKeys?.string1) {
        throw new Error("error:keys.server.decrypt.failed");
    }

    //new keys, encrypt private with user's password
    const bundle = await generateUserKeysBundle();
    userKeysPublic = bundle.pub;
    userKeysPrivate = bundle.priv;
    userKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, password);

    localStorage.setItem(lsEncKey, userKeysEncrypted);
    localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic));

    await fetchEncrypted(
        "user/key/update",
        JSON.stringify({
            string1: userKeysEncrypted,
            string2: JSON.stringify(userKeysPublic)
        })
    );

    publishUserKeysGlobals();
    return "success:keys.generated";
}

async function mainJS() {
    if (localStorage.getItem('lang') != null) {
        lang = localStorage.getItem('lang');
    }
    
    await initBlahs();
    await loadEmotes();

    passwordHash = await hashSHA3_512(password);

    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');

window.addEventListener("load", () => {
    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.transition = 'none';
    fixedContextMenu.style.opacity = '0';
    fixedContextMenu.style.left = '0px';
    fixedContextMenu.style.top = '0px';

    fixedContextMenu.classList.add("show");

    let menuRect = fixedContextMenu.getBoundingClientRect();

    let newLeft = rect.right + 10;
    let newTop = rect.top;

    if (newLeft + menuRect.width > window.innerWidth) {
        newLeft = window.innerWidth - menuRect.width - 10;
    }
    if (newTop + menuRect.height > window.innerHeight) {
        newTop = window.innerHeight - menuRect.height - 10;
    }

    fixedContextMenu.style.left = `${newLeft}px`;
    fixedContextMenu.style.top = `${newTop}px`;

    fixedContextMenu.offsetHeight;
    fixedContextMenu.style.transition = '';
    fixedContextMenu.style.opacity = '';
}

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 <= 55 * 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 <= 55 * 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 : '';
    let pinCallBtns = showRoomBar ? `${pinBtnHtml}${callBtnHtml}` : '';

    roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}<space></space><inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}${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 > 55 * rem) {
        await delay(200);
    }


    let pinCallBtns = `${pinBtnHtml}${callBtnHtml}`;
    roomsTopBarTransition.innerHTML = `<inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}`;

    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");
    var dmsWrapper = document.getElementById("dms-wrapper");
    if (dmsWrapper) {
        if (collapseDmsBtn.classList.contains("collapsed")) {
            dmsWrapper.classList.add("collapsed");
        } else {
            dmsWrapper.classList.remove("collapsed");
        }
    }
}

function clickCollapseGroups() {
    var collapseGroupsBtn = document.getElementById("collapse-groups");
    collapseGroupsBtn.classList.toggle("collapsed");
    var groupsWrapper = document.getElementById("groups-wrapper");
    if (groupsWrapper) {
        if (collapseGroupsBtn.classList.contains("collapsed")) {
            groupsWrapper.classList.add("collapsed");
        } else {
            groupsWrapper.classList.remove("collapsed");
        }
    }
}

function clickAddGroup() {
    switchRoomContent("title.create.group", createGroupScreen, false);
}

function clickAddDm() {
    switchRoomContent("title.create.dm", addDmScreen, false);
}

function clickAddAttachment(btn, e) {
    if (e) {
        e.stopPropagation();
    }
    history.pushState({trap: true}, "", location.href);

    if (fixedContextMenu.classList.contains("show")) {
        fixedContextMenu.classList.remove("show");
    } else {
        const rect = btn.getBoundingClientRect();
        showFixedContextMenu(rect, attachmentMenu);
        
        // Let's reposition it so it appears properly anchored above the input if it's too close to the bottom.
        // showFixedContextMenu automatically handles bounds, but it's set to top-right alignment basically.
        // It's perfectly fine to rely on the default bounds checking of showFixedContextMenu.
    }
}


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);
    }
});

function clearContextMenuStyles() {
    let elems = document.querySelectorAll(".chat-message.context-menu-open");
    for (let i = 0; i < elems.length; i++) {
        elems[i].classList.remove("context-menu-open");
    }
}

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 (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
            if (!e.target.closest('sidebarelement')) {
                setActiveSidebarIndicator(currentMainIndicator);
            }
        }
    }

    if (mainScreen && mainScreen.classList.contains('mobile-details')) {
        const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
        if (window.innerWidth <= 55 * rem) {
            if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right') && !e.target.closest('.profile-popup-container')) {
                mobileNavBack();
            }
        }
    }
});
window.addEventListener("popstate", (e) => {
    popstate();
});

const {App} = window.Capacitor?.Plugins || {};
if (App) {
    App.addListener('backButton', () => {
        popstate();
    });
}

function popstate() {
    if (isProfileOpen) {
        closeProfile();
        return;
    }
    if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
        history.back();
        if (App) {
            App.minimizeApp();
        }
    }
    mobileNavBack();
}



sidebarProfileButton.addEventListener("click", (e) => {
    e.stopPropagation();
    history.pushState({trap: true}, "", location.href);

    if (fixedContextMenu.classList.contains("show")) {
        fixedContextMenu.classList.remove("show");
        if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
        setActiveSidebarIndicator(currentMainIndicator);
    } else {
        setActiveSidebarIndicator(sidebarProfileIndicator, true);
        const rect = sidebarProfileButton.getBoundingClientRect();
        showFixedContextMenu({top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left}, profileContextMenu);
    }
});

function logout() {
    localStorage.clear();
    window.location.href = "login/index.html";
}

let isProfileOpen = false;

async function openProfile(accountId) {
    if (fixedContextMenu.classList.contains("show")) {
        fixedContextMenu.classList.remove("show");
        if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
        setActiveSidebarIndicator(currentMainIndicator);
    }
    
    let popupContainer = document.getElementById("profile-popup-container");
    let popupInner = document.getElementById("profile-popup-inner");
    
    showAction('action.profile.loading', 'profile-loading');
    
    try {
        if (!accountId) accountId = window.id; //var fallback
        
        if (accountId.endsWith(';' + host)) {
            accountId = accountId.substring(0, accountId.length - host.length - 1);
        } else if (accountId.endsWith(':' + host)) {
            accountId = accountId.substring(0, accountId.length - host.length - 1);
        }
        
        let nameWithHost = await fetchAsync(`${url}/idtoname?id=${accountId}`);
        if (nameWithHost.startsWith("error:")) nameWithHost = accountId; //api fallback
        
        let nameParts = nameWithHost.split(":");
        let profileName = nameParts[0] || accountId;
        let profileHost = nameParts.slice(1).join(":") || host;
        
        let pfpUrl = await getAvatarUrl(accountId, nameWithHost);
        
        let bannerUrl = `${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.banner`;
        if (accountId === window.id && window.profileUpdateTimestamp) bannerUrl += `&t=${window.profileUpdateTimestamp}`;
        let bannerHtml = `<img src="${bannerUrl}" class="profile-banner" onerror="this.outerHTML='<div class=\\'profile-banner\\'></div>'">`;
        
        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, "&lt;").replace(/>/g, "&gt;")}</div>` : "";
        
        let isOwnProfile = accountId === id;
        let actionBtnHtml = "";
        if (isOwnProfile) {
            actionBtnHtml = `<button class="profile-action-btn" onclick="gotoSettings('profile');closeProfile();">${processBlah('action.edit.profile')}</button>`;
        } else {
            let hasDm = false;
            let dmRoomId = "";
            if (window.cachedDms) {
                try {
                    let parsedDms = JSON.parse(window.cachedDms);
                    if (parsedDms.dms) {
                        for (let dmId in parsedDms.dms) {
                            let parts = dmId.split('_');
                            if (parts.length >= 2) {
                                let accStandard = accountId.replace(':', ';');
                                if (!accStandard.includes(';')) accStandard += ';' + host;
                                
                                let p0Standard = parts[0].replace(':', ';');
                                if (!p0Standard.includes(';')) p0Standard += ';' + host;
                                
                                let p1Standard = parts[1].replace(':', ';');
                                if (!p1Standard.includes(';')) p1Standard += ';' + host;
                                
                                if (p0Standard === accStandard || p1Standard === accStandard) {
                                    hasDm = true;
                                    dmRoomId = dmId;
                                    break;
                                }
                            }
                        }
                    }
                } catch(e){}
            }
            
            if (hasDm) {
                actionBtnHtml = `<button class="profile-action-btn" onclick="openDm('${dmRoomId}', '${nameWithHost}', '${accountId}'); closeProfile();">${processBlah('action.open.dm')}</button>`;
            } else {
                let sentInvite = false;
                let receivedInvite = false;
                if (window.cachedInvites) {
                    let checkInvite = (type) => {
                        if (window.cachedInvites[type]) {
                            try {
                                let invites = JSON.parse(window.cachedInvites[type]);
                                if (invites.dms) {
                                    for(let key in invites.dms) {
                                        let accStandard = accountId.replace(':', ';');
                                        if (!accStandard.includes(';')) accStandard += ';' + host;
                                        
                                        let keyStandard = key.replace(':', ';');
                                        if (!keyStandard.includes(';')) keyStandard += ';' + host;
                                        
                                        if (keyStandard === accStandard) return true;
                                    }
                                }
                            } catch(e){}
                        }
                        return false;
                    };
                    sentInvite = checkInvite('sent');
                    receivedInvite = checkInvite('received');
                }
                
                if (receivedInvite) {
                    actionBtnHtml = `<button class="profile-action-btn" onclick="acceptInvite('${accountId}'); closeProfile();">${processBlah('action.invite.accept')}</button>`;
                } else if (sentInvite) {
                    actionBtnHtml = `<button class="profile-action-btn" onclick="revokeInvite('${accountId}'); closeProfile();">${processBlah('action.invite.cancel')}</button>`;
                } else {
                    actionBtnHtml = `<button class="profile-action-btn" onclick="sendInviteTo('${accountId}'); closeProfile();">${processBlah('action.invite.send')}</button>`;
                }
            }
        }
            
        popupInner.innerHTML = `
            ${bannerHtml}
            <div class="profile-avatar-container">
                <img src="${pfpUrl}" class="profile-avatar">
            </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>
                ${bioHtml}
                ${actionBtnHtml}
            </div>
        `;
        
        clearAction('profile-loading');
        popupContainer.style.display = "flex";
        popupInner.scrollTop = 0;
        popupContainer.offsetHeight;
        popupContainer.classList.add("show");
        isProfileOpen = true;
    } catch (e) {
        clearAction('profile-loading');
        showNotification(processBlah('error.profile.load.failed'), 'error');
        console.error(e);
    }
}

function closeProfile() {
    let popupContainer = document.getElementById("profile-popup-container");
    popupContainer.classList.remove("show");
    isProfileOpen = false;
    setTimeout(() => {
        popupContainer.style.display = "none";
    }, 300);
}

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 onlyId = id.split(":")[0];
                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};${onlyId}`;
                    actions = `
                    <button class="icon-button" style="background-color: var(--big-green);" onclick="acceptInvite('${id}')">
                        ${ICONS.check({ fill: "#fff", width: "24", height: "24" })}
                    </button>
                    <button class="icon-button" style="background-color: var(--big-red);" onclick="declineInvite('${id}')">
                        ${ICONS.close({ fill: "#fff", width: "24", height: "24" })}
                    </button>
                `;
                } else {
                    desc = `:desc.invite.dm.sent:${username};${onlyId}`;
                    actions = `
                    <button class="icon-button" style="background-color: var(--big-red);" onclick="revokeInvite('${id}')">
                        ${ICONS.close({ fill: "#fff", width: "24", height: "24" })}
                    </button>
                `;
                }

                let entry = invitesEntry
                    .replaceAll("{username}", username.split(':')[0])
                    .replaceAll("{pfp}", pfp)
                    .replaceAll("{desc}", desc)
                    .replaceAll("{actions}", actions)
                    .replaceAll("{id}", id);

                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);
    }
}

window.cachedInvites = {'received': null, 'sent': null};

async function loadInvites(tab) {
    try {
        showAction(`action.fetching.invites.${tab === 'received' ? 'recv' : 'sent'}`, `fetching.invites.${tab}`);
        if (window.cachedInvites[tab]) {
            await renderInvites(window.cachedInvites[tab], tab);
        }
        let res = await fetchEncrypted(`user/invites/${tab}`);
        window.cachedInvites[tab] = res;
        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 sendInviteTo(targetId) {
    try {
        showAction("action.invite.sending", "invite.action");
        let res = await fetchEncrypted("user/dm/invite", targetId);
        clearAction("invite.action");
        showBlahNotification(res || "success:invite.sent");
        if (typeof loadInvites === 'function') await loadInvites('sent');
    } catch (e) {
        clearAction("invite.action");
        showBlahNotification("error:something.wrong");
    }
}

async function acceptInvite(targetId) { //TODO: Implement key generation
    try {
        showAction("action.invite.accepting", "invite.action");

        await ensureUserKeys();

        const inviterPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${targetId}`);
        const inviterPublicKeys = JSON.parse(inviterPublicKeysRaw);

        //generate room key
        const roomKey = randomRoomKey256Chars();

        //encrypt for self
        const selfWrapped = await wrapRoomKeyForSelf(roomKey);

        //encrypt for inviter
        const hybrid = await deriveHybridKeyToUser(inviterPublicKeys);
        const encForInviter = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes);
        const setupPayload = JSON.stringify({
            v: 1,
            alg: "hybrid-x25519-mlkem768",
            ctMlKem: hybrid.ciphertextMlKemBase64,
            enc: encForInviter
        });

        let payload = JSON.stringify({
            string1: targetId,
            string2: selfWrapped,
            string3: setupPayload
        });
        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 > 55 * 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);
}

async function gotoHome(splash = true) {
    if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar
    setActiveSidebarIndicator(sidebarHomeIndicator);
    if (splash) {
        switchRoomContent("title.splash", splashScreen, false);
    }
    switchRoomsBar("title.home", homeRoomBar);
    setActiveRoombarItem(null);
    setupWebSocket();
    await refreshDms(210);
}

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;

let pressDate;

document.addEventListener('touchstart', e => {
    touchStartX = e.changedTouches[0].screenX;
    touchStartY = e.changedTouches[0].screenY;
    touchMoved = false;
    
    activeTouchButton = e.target.closest('button, input, textarea');
    if (activeTouchButton) {
        activeTouchButton.classList.add('is-pressed');
        pressDate = Date.now();
    }
}, {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 (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
                document.activeElement.blur();
            }

            if (activeTouchButton) {
                activeTouchButton.classList.remove('is-pressed');
                activeTouchButton = null;
            }
        }
    }
}, {passive: true});

document.addEventListener('touchend', async (e) => {
    touchEndX = e.changedTouches[0].screenX;
    touchEndY = e.changedTouches[0].screenY;
    
    isAdjusting = false; //resize scroll fix
    
    handleMobileSwipe();

    if (activeTouchButton) {
        
        if (!touchMoved) {
            if (activeTouchButton.tagName !== 'INPUT' && activeTouchButton.tagName !== 'TEXTAREA') {
                if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
                    document.activeElement.blur();
                }

                if (e.cancelable) {
                    e.preventDefault();
                }
                activeTouchButton.click();
            } else {
                activeTouchButton.focus();
            }
        }

        let elapsed = Date.now() - pressDate; //nie pomoglo ale zostawie
        if (elapsed < 100) {
            const remaining = 100 - elapsed;
            await delay(remaining);
        }
        activeTouchButton.classList.remove('is-pressed');
        
        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 > 55 * 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();
            }
        }
    }
}

async function renderDms(res) {
    let container = document.getElementById("dms-list");
    if (!container) return;

    if (res && res !== "") {
        let parsed = JSON.parse(res);
        let dms = parsed.dms || {};
        let dmEntries = Object.entries(dms);

        //Sort by timestamp descending
        dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));

        let html = "";
        let count = 0;

        for (let [dmId, dmData] of dmEntries) {
            let parts = dmId.split('_');
            let myId = `${id};${host}`;
            let targetId = 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 isRead = dmData.string3 === "true";
            let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";

            html += `
            <button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', '${targetUsername}', '${targetId}')">
                <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>
                </div>
            </button>
            `;
            count++;
        }

        if (count > 0) {
            container.innerHTML = html;
            container.classList.remove("empty");
        } else {
            container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
            container.classList.add("empty");
            let blahTags = container.getElementsByTagName("blah");
            for (let i = 0; i < blahTags.length; i++) {
                blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
            }
        }
    } else {
        container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
        container.classList.add("empty");
        let blahTags = container.getElementsByTagName("blah");
        for (let i = 0; i < blahTags.length; i++) {
            blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
        }
    }
}

var currentDmId = null;
var currentDmKey = null; // AES key for encrypting/decrypting messages
var dmUsernameCache = {};
var dmPfpCache = {};
var loadedMessages = {};
var oldestLoadedMsgId = null;
var isLoadingOlderMessages = false;




function escapeHtml(unsafe) {
    if (!unsafe) return "";
    return unsafe
        .replaceAll(/&/g, "&amp;")
        .replaceAll(/</g, "&lt;")
        .replaceAll(/>/g, "&gt;")
        .replaceAll(/"/g, "&quot;")
        .replaceAll(/'/g, "&#039;");
}

async function generateDmMembersHtml(targetId, targetUsername) {
    let myIdStr = id + ";" + host;
    let myUsername = await fetchAsync(`${url}/idtoname?id=${myIdStr}`);
    let myPfp = await getAvatarUrl(myIdStr, myUsername);

    let targetPfp = await getAvatarUrl(targetId, targetUsername);
    let targetDisplayName = targetUsername.split(':')[0];
    let myDisplayName = myUsername.split(':')[0];

    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;">
            <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>
            </div>
        </button>
        <button class="room-entry" onclick="openProfile('${myIdStr}')" 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>
            </div>
        </button>
    </div>
    `;
}

async function openDm(dmId, username, targetId) {
    try {
        showAction("action.dm.opening", "dmopen");

        markDmAsRead(dmId);

        currentDmKey = await ensureDmRoomKey(dmId);
        if (!currentDmKey) {
            throw new Error("Missing DM room key");
        }
        currentDmId = dmId;
        loadedMessages = {};
        oldestLoadedMsgId = null;
        isLoadingOlderMessages = false;

        let pfp = await getAvatarUrl(targetId, username);
        let iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;

        if (currentRoomsBarTitle !== "title.home") {
            gotoHome(false);
        }

        await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
        let membersHtml = await generateDmMembersHtml(targetId, username);
        await switchDetailsContent("title.details", membersHtml);

        let msgContainer = document.getElementById("chat-messages");
        if (msgContainer) {
            msgContainer.innerHTML = `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.messages.loading</blah></div>`;
            let blahTags = msgContainer.getElementsByTagName("blah");
            for (let i = 0; i < blahTags.length; i++) {
                blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
            }
        }

        await loadDmMessages(dmId);
        setupChatScrollListener();

        if (dmMessagePollInterval) clearInterval(dmMessagePollInterval);
        dmMessagePollInterval = setInterval(() => {
            if (currentDmId === dmId) {
                loadDmMessages(dmId);
            } else {
                clearInterval(dmMessagePollInterval);
            }
        }, 15000);

        setActiveRoombarItem(`dm-btn-${dmId}`);
        clearAction("dmopen");
    } catch (e) {
        clearAction("dmopen");
        console.error(e);
        if (e.message === "error:keys.local.server.mismatch" || e.message === "error:keys.server.decrypt.failed") {
            showBlahNotification(e.message);
        } else {
            showBlahNotification("error:dm.open.failed");
        }
    }
}

async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
    try {
        let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset});
        let res = await fetchEncrypted("dm/messages/get", payload);

        if (res && res.startsWith("error:")) {
            showBlahNotification(res);
            return;
        }

        let messages = JSON.parse(res);
        let ids = Object.keys(messages).map(id => parseInt(id));
        if (ids.length > 0) {
            let minId = Math.min(...ids);
            if (oldestLoadedMsgId === null || minId < oldestLoadedMsgId) {
                oldestLoadedMsgId = minId;
            }
        }
        if (ids.length < 50 && startOffset !== "") {
            oldestLoadedMsgId = 0;
        }

        Object.assign(loadedMessages, messages);
        await renderMessages(loadedMessages, isPrepend);
    } catch (e) {
        console.error(e);
        showBlahNotification("error:dm.messages.fetch.failed");
    }
}

function formatBytes(bytes, decimals = 2) {
    if (!+bytes) return '0 Bytes';
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}

function truncateFilename(name, maxLength = 20) {
    if (name.length <= maxLength) return name;
    let extIndex = name.lastIndexOf(".");
    if (extIndex === -1 || extIndex === 0) {
        return name.substring(0, Math.floor(maxLength / 2) - 1) + "..." + name.substring(name.length - Math.floor(maxLength / 2) + 2);
    }
    let ext = name.substring(extIndex);
    let baseName = name.substring(0, extIndex);
    let allowedBaseLength = maxLength - ext.length - 3; // 3 for "..."
    if (allowedBaseLength <= 1) {
        return name.substring(0, maxLength - 3) + "...";
    }
    let half = Math.floor(allowedBaseLength / 2);
    return baseName.substring(0, half) + "..." + baseName.substring(baseName.length - (allowedBaseLength - half)) + ext;
}

async function renderMessages(messages, isPrepend = false) {
    let container = document.getElementById("chat-messages");
    if (!container) return;

    let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));

    let html = "";
    let lastAuthor = null;

    for (let [msgId, msg] of entries) {
        let content = msg.content;
        let reactions = msg.reactions || "";
        let decrypted = false;
        if (msg.key && msg.key !== "") {
            try {
                let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
                content = await decryptAesGcmFromBase64(content, dmKeyBytes);
                if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
                decrypted = true;
            } catch (e) {
                content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
                decrypted = false;
            }
        } else {
            decrypted = true;
        }

        let isRedacted = msg.type === "larp.redacted";
        let redactedStyle = isRedacted ? ` opacity: 0.5; font-style: italic;` : ``;
        let redactedIcon = isRedacted ? `${ICONS.delete({ fill: "currentColor", width: "24", height: "1rem", style: "vertical-align: -0.165em; margin-right: 0.2rem;" })}` : ``;

        let attachmentsHtml = "";
        if (msg.attachment && msg.attachment !== "") {
            if (msg.attachment.startsWith("[")) {
                try {
                    let atts = JSON.parse(msg.attachment);
                    if (atts.length > 0) {
                        attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">`;
                        for (let att of atts) {
                            let attContainerId = `att-container-${msgId}-${att.id}`;
                            let isMedia = att.type.startsWith("image/") || att.type.startsWith("video/");
                            let sizeStr = att.size ? formatBytes(att.size) : "";
                            let displayName = truncateFilename(att.name, 25);
                            let iconSvg = isMedia 
                                ? `${ICONS.image({ width: "24", height: "24", stroke: "currentColor" })}`
                                : `${ICONS.download({ width: "24", height: "24", stroke: "currentColor" })}`;

                            attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="downloadAndRenderAttachment('${msgId}', '${att.id}', '${escapeHtml(att.type)}', '${escapeHtml(att.name)}')">
                                <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>
                                    ${sizeStr ? `<div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.2rem;">${escapeHtml(sizeStr)}</div>` : ""}
                                </div>
                            </div>`;
                        }
                        attachmentsHtml += `</div>`;
                    }
                } catch (e) {
                    console.error("Failed to parse attachments", e);
                }
            } else {
                attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
                    <div style="padding: 0.5rem; background: rgba(0,0,0,0.2); border: var(--border-width) solid var(--border-color); border-radius: 0.5rem; font-size: 0.8rem; opacity: 0.7;">[${processBlah("attachment.legacy")}: ${escapeHtml(msg.attachment)}]</div>
                </div>`;
            }
        }

        if (decrypted) {
            content = escapeHtml(content);
        }
        content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
            return processBlah(p1);
        });

        // Also still process the whole thing in case it's just "dm.begin.notice" without {blah()}
        content = processBlah(content);

        if (msg.type === "larp.info") {
            html += `<div style="text-align: center; opacity: 0.5; padding: 1rem; font-size: 0.9rem;">${content}</div>`;
            lastAuthor = null;
        } else {
            let showAvatar = lastAuthor !== msg.author;
            lastAuthor = msg.author;

            let authorName = "Unknown";
            let pfp = "assets/default_avatar.png";
            if (msg.author !== "0") {
                if (!dmUsernameCache[msg.author]) {
                    let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`);
                    if (fullUsername && !fullUsername.startsWith("error:")) {
                        dmUsernameCache[msg.author] = fullUsername;
                        dmPfpCache[msg.author] = await getAvatarUrl(msg.author, fullUsername);
                    }
                }
                if (dmUsernameCache[msg.author] && !dmPfpCache[msg.author]) {
                    dmPfpCache[msg.author] = await getAvatarUrl(msg.author, dmUsernameCache[msg.author]);
                }
                if (dmUsernameCache[msg.author]) {
                    authorName = dmUsernameCache[msg.author].split(':')[0];
                    pfp = dmPfpCache[msg.author];
                }
            }

            let date = new Date(parseInt(msg.timestamp));
            let timeStr = date.toLocaleString();

            let extraClass = showAvatar ? "with-avatar" : "";
            
            let unencryptedIcon = (!decrypted && !isRedacted) ? `<span title="${processBlah('warning.unencrypted')}" style="color: var(--big-red); vertical-align: -0.165em; margin-right: 0.2rem; cursor: help;">${ICONS.lockOpen({ width: "1rem", height: "1rem", stroke: "currentColor" })}</span>` : ``;

            let repliedHtml = "";
            if (msg.responded && msg.responded !== "") {
                let respondedId = msg.responded;
                if (typeof respondedId === 'number') respondedId = respondedId.toString();
                let respondedAuthorId = null;
                if (typeof respondedId === 'string' && respondedId.includes(':')) {
                    let parts = respondedId.split(':');
                    respondedId = parts[0];
                    respondedAuthorId = parts[1];
                }

                let replyAuthor = processBlah('unknown.author');
                let needsAsyncFetch = false;

                if (loadedMessages[respondedId] && loadedMessages[respondedId].author !== "0") {
                    let rAuthId = loadedMessages[respondedId].author;
                    if (dmUsernameCache[rAuthId]) {
                        replyAuthor = dmUsernameCache[rAuthId].split(':')[0];
                    } else {
                        needsAsyncFetch = true;
                        respondedAuthorId = rAuthId;
                    }
                } else if (respondedAuthorId && dmUsernameCache[respondedAuthorId]) {
                    replyAuthor = dmUsernameCache[respondedAuthorId].split(':')[0];
                } else {
                    needsAsyncFetch = true;
                }

                if (window.replyAuthorCache && window.replyAuthorCache[respondedId]) {
                    replyAuthor = window.replyAuthorCache[respondedId];
                    needsAsyncFetch = false;
                }

                let randId = "";
                if (needsAsyncFetch) {
                    if (!window.replyAuthorCache) window.replyAuthorCache = {};
                    window.replyIdCounter = (window.replyIdCounter || 0) + 1;
                    if (window.replyIdCounter > 1000000) window.replyIdCounter = 0;
                    randId = "reply-author-" + window.replyIdCounter;
                    replyAuthor = processBlah('unknown.author');
                    setTimeout(async () => {
                        try {
                            let aId = respondedAuthorId;
                            if (!aId) {
                                let payload = JSON.stringify({
                                    string1: currentDmId,
                                    string2: "1",
                                    string3: respondedId
                                });
                                let res = await fetchEncrypted("dm/messages/get", payload);
                                if (res && !res.startsWith("error:")) {
                                    let msgs = JSON.parse(res);
                                    if (msgs[respondedId] && msgs[respondedId].author !== "0") {
                                        aId = msgs[respondedId].author;
                                    }
                                }
                            }
                            if (aId) {
                                let fname = dmUsernameCache[aId];
                                if (!fname) {
                                    fname = await fetchAsync(`${url}/idtoname?id=${aId}`);
                                    if (fname && !fname.startsWith("error:")) dmUsernameCache[aId] = fname;
                                }
                                if (fname && !fname.startsWith("error:")) {
                                    let sName = fname.split(':')[0];
                                    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}`;
                                    }
                                }
                            }
                        } catch (e) {
                        }
                    }, 10);
                }

                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="scrollToMessage('${respondedId}')">${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}</div>`;
            }

            let reactionsHtml = "";
            if (reactions) {
                try {
                    let rxArr = JSON.parse(reactions);
                    if (rxArr.length > 0) {
                        let rxCounts = {};
                        for (let rx of rxArr) {
                            let parts = rx.split(':');
                            if (parts.length < 2) continue;
                            let rxVal = parts.slice(1).join(':');
                            if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false};
                            rxCounts[rxVal].count++;
                            if (parts[0] === id) rxCounts[rxVal].me = true;
                        }

                        let rxKeys = Object.keys(rxCounts);
                        if (rxKeys.length > 0) {
                            reactionsHtml = `<div class="chat-message-reactions" style="display: flex; gap: 0.3rem; flex-wrap: wrap; margin-top: 0.3rem;">`;
                            for (let rxVal of rxKeys) {
                                let style = rxCounts[rxVal].me ? "background: rgba(255, 255, 255, 0.12); border-color: var(--text-color);" : "background: rgba(255, 255, 255, 0.06);";
                                let safeRx = encodeURIComponent(rxVal).replace(/'/g, "%27");
                                let escapedRxVal = escapeHtml(rxVal);
                                reactionsHtml += `<span class="reaction-pill" style="padding: 0.1rem 0.4rem; border-radius: 1rem; font-size: 0.85rem; border: var(--border-width) solid var(--light-border-color); cursor: pointer; ${style}" onclick="reactMessagePrompt('${msgId}', '${safeRx}')">${processBlah(escapedRxVal)} ${rxCounts[rxVal].count > 1 ? rxCounts[rxVal].count : ""}</span>`;
                            }
                            reactionsHtml += `</div>`;
                        }
                    }
                } catch (e) {
                }
            }

            html += `
            <div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
                ${showAvatar ? `<img src="${pfp}" class="chat-message-pfp" onclick="openProfile('${msg.author}')" style="cursor: pointer;">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
                <div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')">
                    ${showAvatar ? `<div class="chat-message-header">
                        <span class="chat-message-author" onclick="openProfile('${msg.author}')" style="cursor: pointer;">${authorName}</span>
                        <span class="chat-message-timestamp">${timeStr}</span>
                    </div>` : ""}
                    ${repliedHtml}
                    <div class="chat-message-text" style="${redactedStyle}">${unencryptedIcon}${redactedIcon}${content}</div>
                    ${attachmentsHtml}
                    ${reactionsHtml}
                </div>
            </div>
            `;
        }
    }

    const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
    let wasAtBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < (1.5 * rem);
    if (window.forceScrollToBottom) {
        wasAtBottom = true;
        window.forceScrollToBottom = false;
    }
    let oldScrollTop = container.scrollTop;
    let oldScrollHeight = container.scrollHeight;

    let tempDiv = document.createElement('div');
    tempDiv.innerHTML = html;
    let newChildren = Array.from(tempDiv.children);

    let currentIndex = 0;
    for (let newChild of newChildren) {
        let existingChild = document.getElementById(newChild.id);
        
        if (existingChild && existingChild.parentElement === container) {
            if (container.children[currentIndex] !== existingChild) {
                container.insertBefore(existingChild, container.children[currentIndex]);
            }
            
            let oldText = existingChild.querySelector('.chat-message-text');
            let newText = newChild.querySelector('.chat-message-text');
            if (oldText && newText && oldText.innerHTML !== newText.innerHTML) {
                oldText.innerHTML = newText.innerHTML;
            }

            let oldReactions = existingChild.querySelector('.chat-message-reactions');
            let newReactions = newChild.querySelector('.chat-message-reactions');
            if (newReactions) {
                if (oldReactions) {
                    if (oldReactions.innerHTML !== newReactions.innerHTML) oldReactions.innerHTML = newReactions.innerHTML;
                } else {
                    existingChild.querySelector('.chat-message-content').appendChild(newReactions);
                }
            } else if (oldReactions) {
                oldReactions.remove();
            }
            
            let oldPfp = existingChild.querySelector('.chat-message-pfp');
            let newPfp = newChild.querySelector('.chat-message-pfp');
            if (oldPfp && newPfp && oldPfp.src !== newPfp.src) {
                oldPfp.src = newPfp.src;
            }

            let oldHeader = existingChild.querySelector('.chat-message-header');
            let newHeader = newChild.querySelector('.chat-message-header');
            if (oldHeader && newHeader && oldHeader.innerHTML !== newHeader.innerHTML) {
                oldHeader.innerHTML = newHeader.innerHTML;
            }
            
            let oldReplied = existingChild.querySelector('.chat-message-replied');
            let newReplied = newChild.querySelector('.chat-message-replied');
            if (newReplied) {
                if (oldReplied) {
                    if (oldReplied.innerHTML !== newReplied.innerHTML) oldReplied.innerHTML = newReplied.innerHTML;
                } else {
                    let contentNode = existingChild.querySelector('.chat-message-content');
                    if (oldText) contentNode.insertBefore(newReplied, oldText);
                    else contentNode.insertBefore(newReplied, contentNode.firstChild);
                }
            } else if (oldReplied) {
                oldReplied.remove();
            }

        } else {
            if (container.children[currentIndex]) {
                container.insertBefore(newChild, container.children[currentIndex]);
            } else {
                container.appendChild(newChild);
            }
        }
        currentIndex++;
    }
    
    while (container.children.length > newChildren.length) {
        container.removeChild(container.lastChild);
    }

    let targetScrollTop = 0;
    if (wasAtBottom) {
        targetScrollTop = container.scrollHeight;
    } else if (isPrepend) {
        targetScrollTop = oldScrollTop + (container.scrollHeight - oldScrollHeight);
    } else {
        targetScrollTop = oldScrollTop;
    }

    ignoreScroll = true;
    container.scrollTop = targetScrollTop;
    lastChatScroll = container.scrollHeight - targetScrollTop - container.clientHeight;
    if (lastChatScroll < 0) lastChatScroll = 0;
    
    setTimeout(() => { ignoreScroll = false; }, 50);
}

var lastChatScroll = 0;
var isAdjusting = false;
var ignoreScroll = false;

window.visualViewport?.addEventListener("resize", async () => {
    let container = document.getElementById("chat-messages");
    console.log(container.scrollHeight - container.clientHeight - lastChatScroll)

    isAdjusting = true;
    while (isAdjusting) {
        //chat scroll fix
        ignoreScroll = true;
        container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll;

        //context menu fix
        let menuRect = fixedContextMenu.getBoundingClientRect();
        if (menuRect.bottom > window.innerHeight) {
            fixedContextMenu.style.top = `${Math.max(10, window.innerHeight - menuRect.height - 10)}px`;
        }
        
        requestAnimationFrame(() => {
            ignoreScroll = false;
        });
        await delay(1);
    }
});
function setupChatScrollListener() {
    let container = document.getElementById("chat-messages");
    if (!container) return;

    container.onscroll = async () => {
        
        if (!ignoreScroll) {
            isAdjusting = false;
            lastChatScroll = container.scrollHeight - container.scrollTop - container.clientHeight;
            if (lastChatScroll <= 10) {
                handleDmInteraction();
            }
        }
        
        if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
            isLoadingOlderMessages = true;
            showAction("info.messages.loading.older", "messages.loading.older");
            try {
                await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
                isLoadingOlderMessages = false;
            } catch (e) {
            }
            clearAction("messages.loading.older");
        }
    };
}



async function downloadAndRenderAttachment(msgId, attId, type, name) {
    if (!currentDmId || !currentDmKey) return;
    
    let container = document.getElementById(`att-container-${msgId}-${attId}`);
    if (!container) return;
    
    if (container.dataset.loaded === "true") {
        if (!type.startsWith("image/") && !type.startsWith("video/")) {
            let a = document.createElement("a");
            a.href = container.dataset.url;
            a.download = name;
            a.click();
        }
        return;
    }
    
    if (container.dataset.loading === "true") return;
    container.dataset.loading = "true";
    
    let originalHtml = container.innerHTML;
    container.innerHTML = `<div style="padding: 1rem; color: var(--text-color);">${ICONS.spinner()}</div>`;
    
    try {
        let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
        let nonce = await getNonce(id, passwordHash);
        
        let res = await fetch(`${url}/dm/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, {
            headers: {
                "secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
            }
        });
        
        if (!res.ok) throw new Error("Failed to fetch attachment");
        
        let encryptedBytes = new Uint8Array(await res.arrayBuffer());
        let decryptedBytes = await decryptAesGcmFromBytes(encryptedBytes, dmKeyBytes);
        
        let blob = new Blob([decryptedBytes], {type: type});
        let blobUrl = URL.createObjectURL(blob);
        
        container.dataset.loaded = "true";
        container.dataset.url = blobUrl;
        
        if (type.startsWith("image/")) {
            container.innerHTML = `<img src="${blobUrl}" style="max-width: 100%; max-height: 300px; display: block;" onclick="window.open('${blobUrl}', '_blank')">`;
        } else if (type.startsWith("video/")) {
            container.innerHTML = `<video src="${blobUrl}" controls style="max-width: 100%; max-height: 300px; display: block;"></video>`;
        } else {
            container.innerHTML = originalHtml;
            container.style.border = "1px solid var(--accent-color)";
            let a = document.createElement("a");
            a.href = blobUrl;
            a.download = name;
            a.click();
        }
    } catch (e) {
        console.error(e);
        container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading</div>`;
        container.dataset.loading = "false";
    }
}

let currentDraftAttachments = [];
let replyingToMsgId = null;

function handleAttachmentSelection(event) {
    if (!event.target.files) return;
    for (let file of event.target.files) {
        currentDraftAttachments.push(file);
    }
    event.target.value = '';
    renderAttachmentsPreview();
}

function removeDraftAttachment(index) {
    currentDraftAttachments.splice(index, 1);
    renderAttachmentsPreview();
}

function renderAttachmentsPreview() {
    let previewDiv = document.getElementById("attachments-preview");
    if (!previewDiv) return;
    
    if (currentDraftAttachments.length === 0) {
        previewDiv.style.display = "none";
        previewDiv.innerHTML = "";
        return;
    }
    
    previewDiv.style.display = "flex";
    let html = "";
    currentDraftAttachments.forEach((file, index) => {
        let isImage = file.type.startsWith("image/");
        let isVideo = file.type.startsWith("video/");
        let iconHtml = "";
        if (isImage) {
            iconHtml = `${ICONS.image({ width: "1.5rem", height: "1.5rem", stroke: "currentColor" })}`;
        } else if (isVideo) {
            iconHtml = `${ICONS.video({ width: "1.5rem", height: "1.5rem", stroke: "currentColor" })}`;
        } else {
            iconHtml = `${ICONS.file({ width: "1.5rem", height: "1.5rem", stroke: "currentColor" })}`;
        }
        
        html += `
        <div style="display: flex; align-items: center; gap: 0.8rem; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: var(--border-width) solid rgba(255, 255, 255, 0.1); padding: 0.5rem 0.5rem 0.5rem 1rem; border-radius: 0.75rem; flex-shrink: 0; max-width: 15.625rem; box-shadow: 0 0.25rem 0.375rem rgba(0,0,0,0.1); transition: transform 0.2s ease;">
            <div style="flex-shrink: 0; color: var(--accent-color); display: flex; align-items: center;">${iconHtml}</div>
            <div style="font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-color); font-weight: 500; flex-grow: 1;">${escapeHtml(file.name)}</div>
            <button onclick="removeDraftAttachment(${index})" style="background: transparent; color: var(--text-color); border: none; width: 1.5rem; height: 1.5rem; flex-shrink: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0.125rem; opacity: 0.6; transition: opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.6'">
                ${ICONS.close({ width: "100%", height: "100%", fill: "currentColor", style: "width:100%; height:100%;" })}
            </button>
        </div>`;
    });
    previewDiv.innerHTML = html;
}

async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) {
    if (!currentDmId || !currentDmKey) return;

    let input = document.getElementById("chat-input");
    let content = input.value.trim();
    if (!content && currentDraftAttachments.length === 0) return;

    let pendingMsgId = "pending_" + Date.now();
    let container = document.getElementById("chat-messages");
    if (container) {
        let html = `
        <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>
        </div>
        `;
        container.insertAdjacentHTML('beforeend', html);
        container.scrollTop = container.scrollHeight;
    }

    try {
        let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
        let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
        
        let attachmentsPayload = [];
        if (currentDraftAttachments.length > 0) {
            showAction(processBlah("action.attachment.uploading"), "uploading_atts");
            for (let i = 0; i < currentDraftAttachments.length; i++) {
                let file = currentDraftAttachments[i];
                let arrayBuffer = await file.arrayBuffer();
                let encryptedFileBytes = await encryptAesGcmToBytes(new Uint8Array(arrayBuffer), dmKeyBytes);
                
                let nonce = await getNonce(id, passwordHash);
                let res;
                try {
                    res = await fetch(`${url}/dm/attachment/staging?id=${id}`, {
                        method: "POST",
                        headers: {
                            "secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
                        },
                        body: encryptedFileBytes
                    });
                } catch (e) {
                    throw new Error("error.network.error");
                }
                
                if (res.status === 413) {
                    throw new Error("error.body.too.large");
                }
                let text = await res.text();
                let decText;
                try {
                    decText = await decryptString(text, passwordHash);
                } catch (e) {
                    throw new Error("error:message.send.failed"); // trigger proper blah notification
                }
                
                if (decText && decText.startsWith("success:")) {
                    attachmentsPayload.push({
                        id: i,
                        type: file.type,
                        name: file.name,
                        size: file.size,
                        staging_id: decText.split(":")[1]
                    });
                } else {
                    throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed");
                }
            }
            clearAction("uploading_atts");
        }

        let msgPayload = {
            string1: currentDmId,
            string2: encryptedContent,
            string3: attachmentsPayload.length > 0 ? JSON.stringify(attachmentsPayload) : "",
            string4: replyingToMsgId || ""
        };

        input.value = "";
        input.style.height = '2.5rem';
        sendbutton.style.height = '2.5rem';
        currentDraftAttachments = [];
        renderAttachmentsPreview();

        window.forceScrollToBottom = true;
        replyingToMsgId = null;
        let bar = document.getElementById("replying-bar");
        if (bar) bar.style.display = "none";

        showAction("action.message.sending", "msgsending");

        let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
        clearAction("msgsending");
        if (res && res.startsWith("error:")) {
            showBlahNotification(res);
            let pElem = document.getElementById(pendingMsgId);
            if (pElem) pElem.remove();
        } else if (res && res.startsWith("success:")) {
            let parts = res.split(":");
            if (parts.length > 1 && parts[1] !== "message.sent") {
                let newMsgId = parts[1];
                let finalAttPayload = "";
                if (attachmentsPayload.length > 0) {
                    finalAttPayload = JSON.stringify(attachmentsPayload.map(a => {
                        let clone = Object.assign({}, a);
                        delete clone.staging_id;
                        return clone;
                    }));
                }
                loadedMessages[newMsgId] = {
                    author: id,
                    timestamp: Date.now().toString(),
                    type: "larp.text",
                    content: encryptedContent,
                    attachment: finalAttPayload,
                    key: "0",
                    pervious: "",
                    responded: replyingToMsgId || "",
                    reactions: ""
                };
                renderMessages(loadedMessages);
            }
        }
    } catch (e) {
        clearAction("uploading_atts");
        clearAction("msgsending");
        console.error(e);
        if (e.message && e.message.startsWith("error:")) {
            showBlahNotification(e.message);
        } else {
            showBlahNotification("error:message.send.failed");
        }
        let pElem = document.getElementById(pendingMsgId);
        if (pElem) pElem.remove();
    }
}

function handleChatInputResize(textarea) {
    const sendbutton = textarea.parentElement.children.item(1);

    const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
    textarea.style.transition = 'none';
    textarea.style.height = '0rem';
    sendbutton.style.height = '0rem'; //test
    let borderHeight = textarea.offsetHeight - textarea.clientHeight;
    textarea.style.height = ((textarea.scrollHeight + borderHeight) / rem) + 'rem';
    sendbutton.style.height = ((textarea.scrollHeight + borderHeight) / rem) + 'rem'; //test
    textarea.offsetHeight; // force reflow
    textarea.style.transition = '';
}

function handleChatInputKey(event) {
    if (event.key === "Enter" && !event.shiftKey) {
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        if (!isMobile) {
            event.preventDefault();
            sendMessage();
        }
    }
}

let dmMessagePollInterval = null;
let appWebSocket = null;
let dmMessageLoadTimeout = null;

function setupWebSocket() {
    if (appWebSocket && (appWebSocket.readyState === WebSocket.OPEN || appWebSocket.readyState === WebSocket.CONNECTING)) return;

    let wsUrl = url.replace(/^http/, "ws") + "/ws";
    appWebSocket = new WebSocket(wsUrl);

    appWebSocket.onopen = async () => {
        let release;
        const lock = new Promise(resolve => release = resolve);
        const prevMutex = requestMutex;
        requestMutex = prevMutex.then(() => lock);
        await prevMutex;
        try {
            let nonce = await getNonce(id, passwordHash);
            let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
            appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
        } catch (e) {
            console.error("WS auth failed", e);
        } finally {
            release();
        }
    };

    appWebSocket.onmessage = (event) => {
        let data = event.data;
        if (data.startsWith("dm_message:")) {
            let parts = data.split(":");
            let msgDmId = parts[1];
            let msgType = parts[2] || "unread";
            
            if (msgType === "unread") {
                updateDmListUI(msgDmId, false, Date.now().toString());
            } else if (msgType === "read") {
                updateDmListUI(msgDmId, true, Date.now().toString());
            } else if (msgType === "update") {
                // do nothing to UI sorting/read state
            }
            
            if (currentDmId === msgDmId) {
                if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
                dmMessageLoadTimeout = setTimeout(() => {
                    loadDmMessages(msgDmId);
                }, 300);
            }
        }
    };

    appWebSocket.onclose = () => {
        setTimeout(setupWebSocket, 5000);
    };
}

let currentContextMenuMsgId = null;
let touchContextMenuFired = false;

function handleMessageContextMenu(e, msgId) {
    e.preventDefault();
    if (touchContextMenuFired) {
        touchContextMenuFired = false;
        return;
    }
    if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
    currentContextMenuMsgId = msgId;
    let elem = document.getElementById(`msg-${msgId}`);
    if (elem) elem.classList.add("context-menu-open");

    showFixedContextMenu({top: e.clientY, right: e.clientX, bottom: e.clientY, left: e.clientX}, messageContextMenu);
    let delBtn = fixedContextMenu.querySelector("#context-delete-btn");
    let reactBtn = fixedContextMenu.querySelector("#context-react-btn");
    let msg = loadedMessages[msgId];
    
    if (msg && msg.type === "larp.redacted") {
        if (delBtn) delBtn.style.display = "none";
        if (reactBtn) reactBtn.style.display = "none";
    } else {
        if (delBtn) {
            if (msg && msg.author !== id) {
                delBtn.style.display = "none";
            } else {
                delBtn.style.display = "";
            }
        }
        if (reactBtn) reactBtn.style.display = "";
    }
}

async function deleteMessage(msgId) {
    if (fixedContextMenu) fixedContextMenu.classList.remove("show");
    if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
    showAction("action.message.deleting", "msgdel");
    try {
        let msgPayload = {
            string1: currentDmId,
            string2: msgId
        };
        let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload));
        if (res && res.startsWith("error:")) {
            showBlahNotification(res);
        }
    } catch (e) {
        showBlahNotification("error:message.delete.failed");
    }
    clearAction("msgdel");
}

function cancelReply() {
    replyingToMsgId = null;
    let bar = document.getElementById("replying-bar");
    if (bar) bar.style.display = "none";
    let input = document.getElementById("chat-input");
    if (input) input.focus();
}

async function scrollToMessage(msgId) {
    let elem = document.getElementById(`msg-${msgId}`);
    if (elem) {
        elem.scrollIntoView({behavior: "smooth", block: "center"});
        elem.style.transition = "background-color 0.5s";
        let oldBg = elem.style.backgroundColor;
        elem.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
        setTimeout(() => {
            elem.style.backgroundColor = oldBg;
        }, 1500);
    } else {
        if (oldestLoadedMsgId !== null && parseInt(msgId) < oldestLoadedMsgId) {
            showAction("info.messages.loading.older", "msgscroll");
            let tries = 0;
            while (!document.getElementById(`msg-${msgId}`) && oldestLoadedMsgId > 0 && tries < 15) {
                await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
                tries++;
            }
            clearAction("msgscroll");
            let elem2 = document.getElementById(`msg-${msgId}`);
            if (elem2) {
                elem2.scrollIntoView({behavior: "smooth", block: "center"});
                elem2.style.transition = "background-color 0.5s";
                let oldBg = elem2.style.backgroundColor;
                elem2.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
                setTimeout(() => {
                    elem2.style.backgroundColor = oldBg;
                }, 1500);
            } else {
                showBlahNotification("error:message.not.found");
            }
        }
    }
}

async function replyMessage(msgId) {
    if (fixedContextMenu) fixedContextMenu.classList.remove("show");
    if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();

    let msg = loadedMessages[msgId];
    if (msg && msg.author) {
        replyingToMsgId = msgId + ":" + msg.author;
    } else {
        replyingToMsgId = msgId;
    }

    let bar = document.getElementById("replying-bar");
    if (bar) {
        let msg = loadedMessages[msgId];
        let authorName = processBlah('unknown.author');
        if (msg && msg.author !== "0" && dmUsernameCache[msg.author]) {
            authorName = dmUsernameCache[msg.author].split(':')[0];
        }
        document.getElementById("replying-to-name").textContent = authorName;

        let content = msg ? msg.content : "";
        if (msg && msg.key && msg.key !== "") {
            try {
                let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
                content = await decryptAesGcmFromBase64(content, dmKeyBytes);
            } catch (e) {
            }
        }
        content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => processBlah(p1));
        content = processBlah(content);

        document.getElementById("replying-to-text").textContent = content;
        bar.style.display = "flex";
    }
    let input = document.getElementById("chat-input");
    input.focus();
}

var reactionPickerMenuHtml = "";
var loadedEmotes = [];

async function loadEmotes() {
    try {
        let res = await fetch("emotes.json");
        let emotesObj = await res.json();
        let skinTones = ['\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}'];
        loadedEmotes = Object.keys(emotesObj).flatMap(char => {
            let baseEmote = { char: char, name: emotesObj[char].name, slug: emotesObj[char].slug };
            if (emotesObj[char].skin_tone_support) {
                let variants = skinTones.map(tone => ({
                    char: char + tone,
                    name: emotesObj[char].name + " skin tone",
                    slug: emotesObj[char].slug
                }));
                return [baseEmote, ...variants];
            }
            return [baseEmote];
        });
        
        reactionPickerMenuHtml = `
<div style="display: flex; flex-direction: column; gap: 0.4rem; padding: 0.2rem; min-width: 14rem;">
    <input type="text" id="reaction-picker-search" placeholder="Type text / emoji..." oninput="filterReactions(this.value)" onkeydown="if(event.key === 'Enter') { if(this.value.trim() !== '') reactMessagePrompt(currentContextMenuMsgId, encodeURIComponent(this.value)); }" style="width: auto; align-self: stretch; margin: 0; box-sizing: border-box; padding: 0.5rem 0.6rem; border-radius: 0.65rem; border: var(--border-width) solid var(--light-border-color); background: transparent; color: var(--text-color); font-family: inherit;">
    <div id="reaction-picker-grid" style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.2rem; max-height: 12rem; overflow-y: auto; overflow-x: hidden;">
        ${loadedEmotes.map(emoji => `<button onclick="reactMessagePrompt(currentContextMenuMsgId, encodeURIComponent('${emoji.char}'))" data-name="${emoji.name}" style="min-width: unset; width: 100%; aspect-ratio: 1; display: flex; justify-content: center; align-items: center; padding: 0; font-size: 1.2rem; border-radius: 0.65rem;" class="reaction-emoji-btn">${emoji.char}</button>`).join('')}
    </div>
    <button class="reaction-text-btn" onclick="reactMessagePrompt(currentContextMenuMsgId, encodeURIComponent(document.getElementById('reaction-picker-search').value))" style="margin: 0; padding: 0.5rem; justify-content: center; border-radius: 0.65rem; width: 100%;">
        <span class="blah">title.text.react</span>
    </button>
</div>
`;
    } catch (e) {
        console.error("Failed to load emotes", e);
    }
}

function filterReactions(query) {
    let grid = document.getElementById("reaction-picker-grid");
    if (!grid) return;
    let buttons = grid.querySelectorAll(".reaction-emoji-btn");
    let lowerQuery = query.toLowerCase();
    buttons.forEach(btn => {
        let name = btn.getAttribute("data-name");
        if (name && name.toLowerCase().indexOf(lowerQuery) > -1) {
            btn.style.display = "flex";
        } else {
            btn.style.display = "none";
        }
    });
}

function openReactionPicker(e) {
    if (e) {
        e.stopPropagation();
        e.preventDefault();
    }
    if (!reactionPickerMenuHtml) return;
    let rect = fixedContextMenu.getBoundingClientRect();
    showFixedContextMenu({top: rect.top, right: rect.left, bottom: rect.top, left: rect.left}, reactionPickerMenuHtml);
    let searchInput = document.getElementById('reaction-picker-search');

}

async function reactMessagePrompt(msgId, quickReaction = null) {
    if (fixedContextMenu) fixedContextMenu.classList.remove("show");
    if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
    let reaction = quickReaction;
    if (!reaction) {
        return;
    } else {
        reaction = decodeURIComponent(reaction);
    }

    showAction("action.message.reacting", "msgreact");
    try {
        let currentMsg = loadedMessages[msgId];
        if (currentMsg && currentMsg.type === "larp.redacted") {
            clearAction("msgreact");
            showNotification(processBlah("error.message.already.deleted"), "error");
            return;
        }
        let existingReactions = [];
        if (currentMsg && currentMsg.reactions) {
            let rxStr = currentMsg.reactions;
            if (currentMsg.key && currentMsg.key !== "") {
                let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
                try {
                    rxStr = await decryptAesGcmFromBase64(rxStr, dmKeyBytes);
                } catch (e) {
                }
            }
            try {
                existingReactions = JSON.parse(rxStr);
            } catch (e) {
            }
        }

        let targetEntry = id + ":" + reaction;
        let index = existingReactions.indexOf(targetEntry);
        if (index > -1) {
            existingReactions.splice(index, 1);
        } else {
            existingReactions.push(targetEntry);
        }

        let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
        let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);

        let originalReactionsEncrypted = "";
        if (currentMsg) {
            originalReactionsEncrypted = currentMsg.reactions || "";
            currentMsg.reactions = encryptedReactions;
            renderMessages(loadedMessages);
        }

        let msgPayload = {
            string1: currentDmId,
            string2: msgId,
            string3: encryptedReactions
        };
        let res = await fetchEncrypted("dm/message/react", JSON.stringify(msgPayload));
        if (res && res.startsWith("error:")) {
            showBlahNotification(res);
            if (currentMsg) {
                currentMsg.reactions = originalReactionsEncrypted;
                renderMessages(loadedMessages);
            }
        }
    } catch (e) {
        showBlahNotification("error:message.react.failed");
    }
    clearAction("msgreact");
}

// --- Settings Logic ---
async function gotoSettings(tab = 'account') {
    fixedContextMenu.classList.remove("show");
    if (roomsBarContainer) roomsBarContainer.style.display = "";
    await switchRoomsBar("title.settings", settingsBar);
    
    const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
    if (window.innerWidth > 55 * rem) {
        switchSettingsTab(tab);
    }
}

async function switchSettingsTab(tabName) {
    setActiveRoombarItem('tab-settings-' + tabName);
    if (tabName === 'account') {
        await switchRoomContent("title.account", accountSettings, false);
        let userEl = document.getElementById("settings-username");
        if (userEl && window.username) userEl.value = window.username;
    } else if (tabName === 'profile') {
        showAction('title.loading', 'settings-loading');
        
        try {
            let pfpUrl = await getAvatarUrl(window.id, window.username + ":" + window.host);
            let bannerUrl = `${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.banner`;
            if (window.profileUpdateTimestamp) bannerUrl += `&t=${window.profileUpdateTimestamp}`;
            let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.bio`);
            
            clearAction('settings-loading');
            await switchRoomContent("title.profile", profileSettings, false);
            
            let pfpEl = document.getElementById("settings-avatar-preview");
            if (pfpEl) pfpEl.src = pfpUrl;
            
            let bannerEl = document.getElementById("settings-banner-preview");
            if (bannerEl) bannerEl.src = bannerUrl;
            
            let bioEl = document.getElementById("settings-bio");
            if (bioEl && !bio.startsWith("error:")) bioEl.value = bio;
        } catch (e) {
            clearAction('settings-loading');
            await switchRoomContent("title.profile", profileSettings, false);
        }
    } else if (tabName === 'appearance') {
        await switchRoomContent("title.appearance", appearanceSettings, false);
        populateLanguageDropdown();
    }
}

async function populateLanguageDropdown() {
    let sel = document.getElementById("settings-language");
    if (!sel) return;
    try {
        let path = window.location.pathname.replace("/login/index.html", "/").replace("index.html", "");
        let res = await fetchAsync(`${path}blah/index.json`);
        let json = JSON.parse(res);
        sel.innerHTML = "";
        for (let code in json) {
            let opt = document.createElement("option");
            opt.value = code;
            opt.textContent = json[code].native + " (" + json[code].english + ")";
            if (code === lang) opt.selected = true;
            sel.appendChild(opt);
        }
    } catch (e) {
        console.error("Failed to load languages", e);
    }
}

function changeLanguage(newLang) {
    if (!newLang) return;
    localStorage.setItem('lang', newLang);
    window.location.reload();
}

async function changeUsername() {
    let input = document.getElementById("settings-username");
    let name = input.value;
    if (!name) return;
    showAction("action.account.saving", "accountsave");
    let res = await fetchPostEnc(`${url}/chname?id=${window.id}`, name);
    clearAction("accountsave");
    let text = res;
    if (text.startsWith("success:")) {
        showBlahNotification(text);
        loginUsername = name;
        localStorage.setItem("username", name);
    } else {
        showBlahNotification(text);
    }
}

async function changePassword() {
    let input = document.getElementById("settings-password");
    let newPass = input.value;
    if (!newPass) return;
    showAction("action.account.saving", "accountsave");
    
    let newPassHash = await hashSHA3_512(newPass);
    let newUserKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, newPass);
    
    let payload = JSON.stringify({
        string1: newPassHash,
        string2: JSON.stringify({
            string1: newUserKeysEncrypted,
            string2: JSON.stringify(userKeysPublic)
        })
    });
    
    let res = await fetchPostEnc(`${url}/chpass?id=${window.id}`, payload);
    
    clearAction("accountsave");
    let text = res;
    if (text.startsWith("success:")) {
        password = newPass;
        localStorage.setItem("password", newPass);
        
        passwordHash = newPassHash;
        userKeysEncrypted = newUserKeysEncrypted;
        localStorage.setItem(`userKeys.enc.v1:${window.id}`, userKeysEncrypted);

        showBlahNotification(text);
    } else {
        showBlahNotification(text);
    }
}

async function deleteAccount() {
    if (!confirm(processBlah("desc.delete.account") + "\n\nAre you sure?")) return;
    showAction("action.account.deleting", "accountdel");
    let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, "");
    clearAction("accountdel");
    let text = res;
    if (text.startsWith("success:")) {
        alert(processBlah(text));
        logout();
    } else {
        showBlahNotification(text);
    }
}

async function saveProfile() {
    let bioInput = document.getElementById("settings-bio");
    if (bioInput) {
        let payload = { string1: "larp.profile.bio", string2: bioInput.value || "" };
        showAction("action.bio.saving", "savebio");
        let res = await fetchEncrypted("user/storage/public/update", JSON.stringify(payload));
        clearAction("savebio");
        if (res && res.startsWith("error:")) { showBlahNotification(res); return; }
    }
    showBlahNotification("success:profile.updated");
}

// --- Cropper Logic ---
let cropperType = 'avatar'; // 'avatar' or 'banner'
let cropperImageObj = null;
let cropperPosX = 0;
let cropperPosY = 0;
let cropperBaseZoom = 1;
let cropperOriginalType = '';
let cropperOriginalB64 = '';

let isDraggingCropper = false;
let startDragX = 0;
let startDragY = 0;
let startPosX = 0;
let startPosY = 0;

function openCropper(input, type) {
    if (!input.files || input.files.length === 0) return;
    let file = input.files[0];
    cropperType = type;
    
    let reader = new FileReader();
    reader.onload = (e) => {
        let img = new Image();
        img.onload = () => {
            cropperImageObj = img;
            initCropper();
        };
        cropperOriginalType = file.type;
        cropperOriginalB64 = e.target.result;
        img.src = e.target.result;
        document.getElementById('cropper-image').src = e.target.result;
    };
    reader.readAsDataURL(file);
    input.value = ""; // reset input
}

function initCropper() {
    let container = document.getElementById('cropper-viewport-container');
    let imgEl = document.getElementById('cropper-image');
    let zoomSlider = document.getElementById('cropper-zoom');
    
    // Set aspect ratio
    let targetWidth = cropperType === 'avatar' ? 512 : 494;
    let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
    let aspectRatio = targetWidth / targetHeight;
    
    container.style.height = 'auto';
    container.style.maxHeight = 'calc(85vh - 16rem)';
    container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
    container.style.maxWidth = `min(30rem, calc((85vh - 16rem) * ${aspectRatio}))`;
    
    // Force layout recalculation
    void container.offsetHeight;
    let containerWidth = container.clientWidth || 300;
    
    let imgWidth = cropperImageObj.width;
    let imgHeight = cropperImageObj.height;
    
    let scaleX = containerWidth / imgWidth;
    let scaleY = (containerWidth * (targetHeight / targetWidth)) / imgHeight;
    cropperBaseZoom = Math.max(scaleX, scaleY);
    
    zoomSlider.min = cropperBaseZoom;
    zoomSlider.max = cropperBaseZoom * 5;
    zoomSlider.value = cropperBaseZoom;
    
    cropperPosX = 0;
    cropperPosY = 0;
    
    updateCropperTransform();
    document.getElementById('cropper-popup-container').classList.add('show');
}

function updateCropperZoom() {
    updateCropperTransform();
}

function updateCropperTransform() {
    let zoom = parseFloat(document.getElementById('cropper-zoom').value);
    let imgEl = document.getElementById('cropper-image');
    let container = document.getElementById('cropper-viewport-container');
    
    if (cropperImageObj) {
        let scaledW = cropperImageObj.width * zoom;
        let scaledH = cropperImageObj.height * zoom;
        
        let minX = container.clientWidth - scaledW;
        let maxX = 0;
        let minY = container.clientHeight - scaledH;
        let maxY = 0;
        
        if (minX > maxX) minX = maxX;
        if (minY > maxY) minY = maxY;
        
        if (cropperPosX < minX) cropperPosX = minX;
        if (cropperPosX > maxX) cropperPosX = maxX;
        if (cropperPosY < minY) cropperPosY = minY;
        if (cropperPosY > maxY) cropperPosY = maxY;
    }

    imgEl.style.transform = `translate(${cropperPosX}px, ${cropperPosY}px) scale(${zoom})`;
}

function closeCropper() {
    document.getElementById('cropper-popup-container').classList.remove('show');
}

async function confirmCropper() {
    let zoom = parseFloat(document.getElementById('cropper-zoom').value);
    let container = document.getElementById('cropper-viewport-container');
    
    let targetWidth = cropperType === 'avatar' ? 512 : 988;
    let targetHeight = cropperType === 'avatar' ? 512 : 400;
    let scaleRatio = targetWidth / container.clientWidth;
    let drawX = cropperPosX * scaleRatio;
    let drawY = cropperPosY * scaleRatio;
    let drawW = cropperImageObj.width * zoom * scaleRatio;
    let drawH = cropperImageObj.height * zoom * scaleRatio;

    let b64;
    let newSrc;
    let isAnimated = false;
    let gifFrames = null;
    let webpDecoded = null;
    let parsedGif = null;

    if (cropperOriginalType === 'image/gif') {
        isAnimated = true;
    } else if (cropperOriginalType === 'image/webp') {
        showAction("action.processing", "uploadmedia");
        try {
            let res = await fetch(cropperOriginalB64);
            let buffer = await res.arrayBuffer();
            let webpWorker = new Worker('webp-worker.js', { type: 'module' });
            webpDecoded = await new Promise((resolve, reject) => {
                webpWorker.onmessage = (e) => {
                    if (e.data.success) resolve(e.data.data);
                    else reject(new Error(e.data.error));
                    webpWorker.terminate();
                };
                webpWorker.onerror = (err) => { reject(err); webpWorker.terminate(); };
                webpWorker.postMessage({ action: 'decode', data: new Uint8Array(buffer) });
            });
            if (webpDecoded && webpDecoded.length > 1) {
                isAnimated = true;
            }
        } catch (e) {
            console.error("WebP decode check failed", e);
        }
    }

    if (isAnimated) {
        showAction("action.processing", "uploadmedia");
        try {
            let webpFrames = [];
            let tempCanvas = document.createElement('canvas');
            let tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
            
            let gifCanvas = document.createElement('canvas');
            let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
            gifCanvas.width = targetWidth;
            gifCanvas.height = targetHeight;

            if (cropperOriginalType === 'image/gif') {
                let res = await fetch(cropperOriginalB64);
                let buffer = await res.arrayBuffer();
                parsedGif = gifuct.parseGIF(buffer);
                gifFrames = gifuct.decompressFrames(parsedGif, true);
                
                tempCanvas.width = parsedGif.lsd.width;
                tempCanvas.height = parsedGif.lsd.height;
                
                let frameImageData = null;
                let previousImageData = null;
                
                for (let i = 0; i < gifFrames.length; i++) {
                    let frame = gifFrames[i];
                    
                    if (frame.disposalType === 3) {
                        previousImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
                    }
                    
                    if (!frameImageData || frameImageData.width !== frame.dims.width || frameImageData.height !== frame.dims.height) {
                        frameImageData = tempCtx.createImageData(frame.dims.width, frame.dims.height);
                    }
                    
                    frameImageData.data.set(frame.patch);
                    
                    let patchCanvas = document.createElement('canvas');
                    patchCanvas.width = frame.dims.width;
                    patchCanvas.height = frame.dims.height;
                    patchCanvas.getContext('2d').putImageData(frameImageData, 0, 0);
                    
                    tempCtx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
                    
                    gifCtx.fillStyle = '#000';
                    gifCtx.fillRect(0, 0, targetWidth, targetHeight);
                    gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
                    
                    let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
                    webpFrames.push({
                        data: frameImgData.data,
                        duration: Math.max(20, frame.delay || 0),
                        config: { lossless: 0, quality: 90 }
                    });

                    if (frame.disposalType === 2) {
                        tempCtx.clearRect(frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
                    } else if (frame.disposalType === 3 && previousImageData) {
                        tempCtx.putImageData(previousImageData, 0, 0);
                    }
                }
            } else if (cropperOriginalType === 'image/webp' && webpDecoded) {
                tempCanvas.width = webpDecoded[0].width;
                tempCanvas.height = webpDecoded[0].height;
                for (let i = 0; i < webpDecoded.length; i++) {
                    let frame = webpDecoded[i];
                    let frameImageData = new ImageData(
                        new Uint8ClampedArray(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength),
                        frame.width,
                        frame.height
                    );
                    tempCtx.putImageData(frameImageData, 0, 0);
                    
                    gifCtx.fillStyle = '#000';
                    gifCtx.fillRect(0, 0, targetWidth, targetHeight);
                    gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
                    
                    let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
                    webpFrames.push({
                        data: frameImgData.data,
                        duration: Math.max(20, frame.duration || 0),
                        config: { lossless: 0, quality: 90 }
                    });
                }
            }

            let webpUint8 = await new Promise((resolve, reject) => {
                let worker = new Worker('webp-worker.js', { type: 'module' });
                worker.onmessage = (e) => {
                    if (e.data.success) {
                        resolve(e.data.data);
                    } else {
                        reject(new Error(e.data.error));
                    }
                    worker.terminate();
                };
                worker.onerror = (err) => {
                    reject(err);
                    worker.terminate();
                };
                worker.postMessage({ action: 'encode', targetWidth, targetHeight, webpFrames });
            });
            let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
            
            b64 = await new Promise((resolve) => {
                let reader = new FileReader();
                reader.onload = function() {
                    resolve(reader.result.split(',')[1]);
                };
                reader.readAsDataURL(webpBlob);
            });
            newSrc = 'data:image/webp;base64,' + b64;
        } catch (e) {
            console.error("Animated crop failed", e);
            b64 = cropperOriginalB64.split(',')[1];
            newSrc = cropperOriginalB64;
        }
    } else {
        let canvas = document.createElement('canvas');
        let ctx = canvas.getContext('2d');
        
        canvas.width = targetWidth;
        canvas.height = targetHeight;
        
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, targetWidth, targetHeight);
        ctx.drawImage(cropperImageObj, drawX, drawY, drawW, drawH);
        
        b64 = canvas.toDataURL('image/webp', 0.9).split(',')[1];
        newSrc = 'data:image/webp;base64,' + b64;
    }
    
    let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
    let fileBytes = base64ToUint8(b64);
    showAction("action.uploading", "uploadmedia");
    let res = await fetchEncryptedUpload(payloadKey, fileBytes);
    clearAction("uploadmedia");
    if (res && res.startsWith("error:")) {
        showBlahNotification(res);
    } else {
        showBlahNotification(`success:${cropperType}.updated`);
        window.profileUpdateTimestamp = Date.now();
        
        // Update all matching images on the page instantly
        document.querySelectorAll('img').forEach(img => {
            if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) {
                img.src = newSrc;
            }
        });
    }
    
    closeCropper();
}


let container = document.getElementById('cropper-viewport-container');
if (container) {
    const startDrag = (e) => {
        isDraggingCropper = true;
        startDragX = e.clientX || (e.touches && e.touches[0].clientX);
        startDragY = e.clientY || (e.touches && e.touches[0].clientY);
        startPosX = cropperPosX;
        startPosY = cropperPosY;
        if (e.type !== "touchstart") e.preventDefault();
    };

    const onDrag = (e) => {
        if (!isDraggingCropper) return;
        let clientX = e.clientX || (e.touches && e.touches[0].clientX);
        let clientY = e.clientY || (e.touches && e.touches[0].clientY);
        cropperPosX = startPosX + (clientX - startDragX);
        cropperPosY = startPosY + (clientY - startDragY);
        updateCropperTransform();
    };

    const endDrag = () => {
        isDraggingCropper = false;
    };

    container.addEventListener('mousedown', startDrag);
    container.addEventListener('touchstart', startDrag, {passive: true});
    document.addEventListener('mousemove', onDrag);
    document.addEventListener('touchmove', onDrag, {passive: true});
    document.addEventListener('mouseup', endDrag);
    document.addEventListener('touchend', endDrag);
}

function updateDmListUI(dmId, isRead, timestamp = null) {
    try {
        if (!window.cachedDms) return;
        let parsedDms = JSON.parse(window.cachedDms);
        if (!parsedDms.dms || !parsedDms.dms[dmId]) return;
        
        let changed = false;
        if (parsedDms.dms[dmId].string3 !== (isRead ? "true" : "false")) {
            parsedDms.dms[dmId].string3 = isRead ? "true" : "false";
            changed = true;
        }
        if (timestamp && parsedDms.dms[dmId].string2 !== timestamp) {
            parsedDms.dms[dmId].string2 = timestamp;
            changed = true;
        }
        
        if (changed) {
            window.cachedDms = JSON.stringify(parsedDms);
        }
        
        let btn = document.getElementById(`dm-btn-${dmId}`);
        let container = document.getElementById("dms-list");
        if (btn && container) {
            let nameSpan = btn.querySelector('.room-name');
            if (nameSpan) {
                nameSpan.style.opacity = isRead ? "0.8" : "1";
            }
            if (timestamp) {
                container.prepend(btn);
            }
        } else {
            if (typeof refreshDms === 'function') refreshDms(0, false);
        }
    } catch (e) {
        console.error(e);
        if (typeof refreshDms === 'function') refreshDms(0, false);
    }
}

function markDmAsRead(dmId) {
    fetchEncrypted("dm/read", JSON.stringify({string1: dmId})).catch(()=>{});
    updateDmListUI(dmId, true);
}

function handleDmInteraction() {
    if (!currentDmId) return;
    let container = document.getElementById("chat-messages");
    if (!container) return;
    
    if (lastChatScroll <= 10) {
        try {
            if (window.cachedDms) {
                let parsedDms = JSON.parse(window.cachedDms);
                if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") {
                    markDmAsRead(currentDmId);
                }
            }
        } catch (e) {}
    }
}

document.addEventListener('click', handleDmInteraction);
document.addEventListener('keydown', handleDmInteraction);
document.addEventListener('touchstart', handleDmInteraction);