Changes:
- add replies - add reactions - add message redacting - fix chat height when opening mobile keyboard
This commit is contained in:
parent
75cd78ed5a
commit
4016975900
10 changed files with 1286 additions and 310 deletions
|
|
@ -120,7 +120,6 @@
|
||||||
"title.back.to.register": "back to registration",
|
"title.back.to.register": "back to registration",
|
||||||
"placeholder.username": "cat name",
|
"placeholder.username": "cat name",
|
||||||
"placeholder.captcha.code": "captcha code",
|
"placeholder.captcha.code": "captcha code",
|
||||||
"placeholder.message.input": "meow...",
|
|
||||||
"desc.messages.loading": "loading meows...",
|
"desc.messages.loading": "loading meows...",
|
||||||
"desc.no.dms": "no direct meowchats :c",
|
"desc.no.dms": "no direct meowchats :c",
|
||||||
"action.dm.opening": "opening meowchat...",
|
"action.dm.opening": "opening meowchat...",
|
||||||
|
|
@ -128,5 +127,18 @@
|
||||||
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
|
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
|
||||||
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
|
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
|
||||||
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
||||||
"messages.decrypt.failed": "failed to decrypt meow :c"
|
"messages.decrypt.failed": "failed to decrypt meow :c",
|
||||||
|
"title.reply.message": "meow back",
|
||||||
|
"title.react.message": "purr",
|
||||||
|
"title.delete.message": "hiss away",
|
||||||
|
"title.replied.to": "meowed back to",
|
||||||
|
"action.message.deleting": "hissing message away...",
|
||||||
|
"action.message.sending": "sending meow...",
|
||||||
|
"action.message.reacting": "adding purr...",
|
||||||
|
"error:message.delete.failed": "failed to hiss away meow",
|
||||||
|
"error:message.send.failed": "failed to send meow",
|
||||||
|
"error:message.react.failed": "failed to add purr",
|
||||||
|
"info.sending.message": "sending...",
|
||||||
|
"placeholder.message.input": "meow...",
|
||||||
|
"larp.redacted": "this meow was hissed away."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,5 +127,17 @@
|
||||||
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
|
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
|
||||||
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
|
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
|
||||||
"dm.messages.fetch.failed": "Failed to fetch messages",
|
"dm.messages.fetch.failed": "Failed to fetch messages",
|
||||||
"messages.decrypt.failed": "Failed to decrypt message"
|
"messages.decrypt.failed": "Failed to decrypt message",
|
||||||
|
"title.reply.message": "Reply",
|
||||||
|
"title.react.message": "React",
|
||||||
|
"title.delete.message": "Delete",
|
||||||
|
"title.replied.to": "Replied to",
|
||||||
|
"action.message.deleting": "Deleting message...",
|
||||||
|
"action.message.sending": "Sending message...",
|
||||||
|
"action.message.reacting": "Adding reaction...",
|
||||||
|
"error:message.delete.failed": "Failed to delete message",
|
||||||
|
"error:message.send.failed": "Failed to send message",
|
||||||
|
"error:message.react.failed": "Failed to add reaction",
|
||||||
|
"info.sending.message": "Sending...",
|
||||||
|
"larp.redacted": "This message was deleted."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
var prot = window.location.protocol;
|
var prot = window.location.protocol;
|
||||||
|
|
||||||
async function updateProtocolAndUrl(host)
|
async function updateProtocolAndUrl(host) {
|
||||||
{
|
|
||||||
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
||||||
if (prot == "http:") {
|
if (prot == "http:") {
|
||||||
try {
|
try {
|
||||||
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
try {
|
try {
|
||||||
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
||||||
prot = "https:";
|
prot = "https:";
|
||||||
|
} catch (error) {
|
||||||
}
|
}
|
||||||
catch (error) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
url = `${prot}//${host}/_larpix`;
|
url = `${prot}//${host}/_larpix`;
|
||||||
|
|
@ -148,7 +146,7 @@ async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) {
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
"raw",
|
"raw",
|
||||||
enc.encode(passwordPlain),
|
enc.encode(passwordPlain),
|
||||||
{ name: "PBKDF2" },
|
{name: "PBKDF2"},
|
||||||
false,
|
false,
|
||||||
["deriveKey"]
|
["deriveKey"]
|
||||||
);
|
);
|
||||||
|
|
@ -160,7 +158,7 @@ async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) {
|
||||||
hash: "SHA-256"
|
hash: "SHA-256"
|
||||||
},
|
},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: "AES-GCM", length: 256 },
|
{name: "AES-GCM", length: 256},
|
||||||
false,
|
false,
|
||||||
["encrypt", "decrypt"]
|
["encrypt", "decrypt"]
|
||||||
);
|
);
|
||||||
|
|
@ -172,7 +170,7 @@ async function encryptJsonWithPassword(jsonObj, passwordPlain) {
|
||||||
const iterations = 310000;
|
const iterations = 310000;
|
||||||
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations);
|
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations);
|
||||||
const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj));
|
const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj));
|
||||||
const ciphertextBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
|
const ciphertextBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, plaintext);
|
||||||
const ciphertext = new Uint8Array(ciphertextBuf);
|
const ciphertext = new Uint8Array(ciphertextBuf);
|
||||||
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
|
|
@ -195,7 +193,7 @@ async function decryptJsonWithPassword(envelopeJson, passwordPlain) {
|
||||||
const iv = base64ToUint8(env.iv);
|
const iv = base64ToUint8(env.iv);
|
||||||
const ct = base64ToUint8(env.ct);
|
const ct = base64ToUint8(env.ct);
|
||||||
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter);
|
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter);
|
||||||
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
const plainBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
|
||||||
return JSON.parse(new TextDecoder().decode(plainBuf));
|
return JSON.parse(new TextDecoder().decode(plainBuf));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +207,7 @@ async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length =
|
||||||
const salt = saltBytes ?? new Uint8Array(32);
|
const salt = saltBytes ?? new Uint8Array(32);
|
||||||
const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]);
|
const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]);
|
||||||
const bits = await crypto.subtle.deriveBits(
|
const bits = await crypto.subtle.deriveBits(
|
||||||
{ name: "HKDF", hash: "SHA-256", salt, info },
|
{name: "HKDF", hash: "SHA-256", salt, info},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
length * 8
|
length * 8
|
||||||
);
|
);
|
||||||
|
|
@ -217,14 +215,14 @@ async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length =
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importAesGcmKey(keyBytes) {
|
async function importAesGcmKey(keyBytes) {
|
||||||
return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
return crypto.subtle.importKey("raw", keyBytes, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function encryptAesGcmToBase64(plainText, keyBytes) {
|
async function encryptAesGcmToBase64(plainText, keyBytes) {
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
const key = await importAesGcmKey(keyBytes);
|
const key = await importAesGcmKey(keyBytes);
|
||||||
const pt = new TextEncoder().encode(plainText);
|
const pt = new TextEncoder().encode(plainText);
|
||||||
const ctBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt);
|
const ctBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, pt);
|
||||||
const combined = new Uint8Array(iv.length + ctBuf.byteLength);
|
const combined = new Uint8Array(iv.length + ctBuf.byteLength);
|
||||||
combined.set(iv, 0);
|
combined.set(iv, 0);
|
||||||
combined.set(new Uint8Array(ctBuf), iv.length);
|
combined.set(new Uint8Array(ctBuf), iv.length);
|
||||||
|
|
@ -236,7 +234,7 @@ async function decryptAesGcmFromBase64(cipherBase64, keyBytes) {
|
||||||
const iv = combined.slice(0, 12);
|
const iv = combined.slice(0, 12);
|
||||||
const ct = combined.slice(12);
|
const ct = combined.slice(12);
|
||||||
const key = await importAesGcmKey(keyBytes);
|
const key = await importAesGcmKey(keyBytes);
|
||||||
const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
const ptBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
|
||||||
return new TextDecoder().decode(ptBuf);
|
return new TextDecoder().decode(ptBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -704,7 +702,7 @@ function clearAction(actionid) {
|
||||||
|
|
||||||
notif.addEventListener('transitionend', async () => {
|
notif.addEventListener('transitionend', async () => {
|
||||||
notif.remove();
|
notif.remove();
|
||||||
}, { once: true });
|
}, {once: true});
|
||||||
setTimeout(() => notif.remove(), 300);
|
setTimeout(() => notif.remove(), 300);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -744,6 +742,7 @@ function getInitials(name) {
|
||||||
//1 word at least 2 letters
|
//1 word at least 2 letters
|
||||||
return word.substring(0, 2).toUpperCase();
|
return word.substring(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringToColor(str) {
|
function stringToColor(str) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
|
@ -755,6 +754,7 @@ function stringToColor(str) {
|
||||||
|
|
||||||
return `hsl(${h}, ${s}%, ${l}%)`;
|
return `hsl(${h}, ${s}%, ${l}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAvatarSvg(name, size = 512) {
|
function createAvatarSvg(name, size = 512) {
|
||||||
const initials = getInitials(name);
|
const initials = getInitials(name);
|
||||||
const color = stringToColor(name);
|
const color = stringToColor(name);
|
||||||
|
|
@ -777,17 +777,14 @@ function createAvatarSvg(name, size = 512) {
|
||||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvatarUrl(id, username)
|
async function getAvatarUrl(id, username) {
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
|
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
|
||||||
if ((await fetchAsync(pfpUrl)) == "")
|
if ((await fetchAsync(pfpUrl)) == "") {
|
||||||
{
|
|
||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
return pfpUrl;
|
return pfpUrl;
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
return createAvatarSvg(username);
|
return createAvatarSvg(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -810,6 +807,7 @@ async function loadingFadeOut() {
|
||||||
mainScreen.style.transform = "";
|
mainScreen.style.transform = "";
|
||||||
mainScreen.style.opacity = "";
|
mainScreen.style.opacity = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadingFadeIn() {
|
async function loadingFadeIn() {
|
||||||
|
|
||||||
loadingScreen.style.transform = "scale(0.85)";
|
loadingScreen.style.transform = "scale(0.85)";
|
||||||
|
|
@ -852,8 +850,7 @@ async function initBlahs() {
|
||||||
|
|
||||||
let placeholders = document.querySelectorAll("[placeholder]");
|
let placeholders = document.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -898,7 +895,7 @@ function processBlah(blahmessage) {
|
||||||
let valueslist = "";
|
let valueslist = "";
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
let value = processBlah(values[i]);
|
let value = processBlah(values[i]);
|
||||||
valueslist+=`${value}, `;
|
valueslist += `${value}, `;
|
||||||
message = message.replaceAll(`{${i}}`, value);
|
message = message.replaceAll(`{${i}}`, value);
|
||||||
}
|
}
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
|
|
@ -908,9 +905,7 @@ function processBlah(blahmessage) {
|
||||||
message = message.replaceAll('{all}', valueslist);
|
message = message.replaceAll('{all}', valueslist);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
} catch (e) {
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
if (prepended) {
|
if (prepended) {
|
||||||
return blahmessage.substring(1);
|
return blahmessage.substring(1);
|
||||||
}
|
}
|
||||||
|
|
@ -962,7 +957,7 @@ async function generateUserKeysBundle() {
|
||||||
privX25519: uint8ToBase64(privX25519),
|
privX25519: uint8ToBase64(privX25519),
|
||||||
privMlKem: uint8ToBase64(privMlKem)
|
privMlKem: uint8ToBase64(privMlKem)
|
||||||
};
|
};
|
||||||
return { pub, priv };
|
return {pub, priv};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureUserKeys() {
|
async function ensureUserKeys() {
|
||||||
|
|
@ -1082,6 +1077,7 @@ async function mainJS() {
|
||||||
|
|
||||||
await start();
|
await start();
|
||||||
}
|
}
|
||||||
|
|
||||||
id = localStorage.getItem('id');
|
id = localStorage.getItem('id');
|
||||||
username = localStorage.getItem('username');
|
username = localStorage.getItem('username');
|
||||||
password = localStorage.getItem('password');
|
password = localStorage.getItem('password');
|
||||||
|
|
@ -1104,8 +1100,7 @@ function showFixedContextMenu(rect, html) {
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1114,10 +1109,31 @@ function showFixedContextMenu(rect, html) {
|
||||||
|
|
||||||
fixedContextMenu.innerHTML = doc.body.innerHTML;
|
fixedContextMenu.innerHTML = doc.body.innerHTML;
|
||||||
|
|
||||||
fixedContextMenu.style.left = `${rect.right + 10}px`;
|
fixedContextMenu.style.transition = 'none';
|
||||||
fixedContextMenu.style.top = `${rect.top}px`;
|
fixedContextMenu.style.opacity = '0';
|
||||||
|
fixedContextMenu.style.left = '0px';
|
||||||
|
fixedContextMenu.style.top = '0px';
|
||||||
|
|
||||||
fixedContextMenu.classList.add("show");
|
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 currentRoomsBarTitle = null;
|
||||||
|
|
@ -1156,8 +1172,7 @@ async function switchRoomsBar(title, content) {
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1171,10 +1186,11 @@ async function switchRoomsBar(title, content) {
|
||||||
roomsTopBarTransition.style.transform = "";
|
roomsTopBarTransition.style.transform = "";
|
||||||
roomsTopBarTransition.style.opacity = "";
|
roomsTopBarTransition.style.opacity = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentRoomContentTitle = null;
|
var currentRoomContentTitle = null;
|
||||||
var currentRoomContentHtml = null;
|
var currentRoomContentHtml = null;
|
||||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false)
|
|
||||||
{
|
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
||||||
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
||||||
|
|
@ -1203,9 +1219,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
mainScreen.classList.add('mobile-content');
|
mainScreen.classList.add('mobile-content');
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
await delay(200);
|
await delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1229,8 +1243,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1246,10 +1259,11 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentDetailsContentTitle = null;
|
var currentDetailsContentTitle = null;
|
||||||
var currentDetailsContentHtml = null;
|
var currentDetailsContentHtml = null;
|
||||||
async function switchDetailsContent(title, content)
|
|
||||||
{
|
async function switchDetailsContent(title, content) {
|
||||||
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
|
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
|
||||||
currentDetailsContentTitle = title;
|
currentDetailsContentTitle = title;
|
||||||
currentDetailsContentHtml = content;
|
currentDetailsContentHtml = content;
|
||||||
|
|
@ -1284,8 +1298,7 @@ async function switchDetailsContent(title, content)
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1301,8 +1314,8 @@ async function switchDetailsContent(title, content)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
function clickCollapseDms()
|
|
||||||
{
|
function clickCollapseDms() {
|
||||||
var collapseDmsBtn = document.getElementById("collapse-dms");
|
var collapseDmsBtn = document.getElementById("collapse-dms");
|
||||||
collapseDmsBtn.classList.toggle("collapsed");
|
collapseDmsBtn.classList.toggle("collapsed");
|
||||||
var dmsWrapper = document.getElementById("dms-wrapper");
|
var dmsWrapper = document.getElementById("dms-wrapper");
|
||||||
|
|
@ -1314,8 +1327,8 @@ function clickCollapseDms()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clickCollapseGroups()
|
|
||||||
{
|
function clickCollapseGroups() {
|
||||||
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
||||||
collapseGroupsBtn.classList.toggle("collapsed");
|
collapseGroupsBtn.classList.toggle("collapsed");
|
||||||
var groupsWrapper = document.getElementById("groups-wrapper");
|
var groupsWrapper = document.getElementById("groups-wrapper");
|
||||||
|
|
@ -1327,12 +1340,12 @@ function clickCollapseGroups()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clickAddGroup()
|
|
||||||
{
|
function clickAddGroup() {
|
||||||
switchRoomContent("title.create.group", createGroupScreen, false);
|
switchRoomContent("title.create.group", createGroupScreen, false);
|
||||||
}
|
}
|
||||||
function clickAddDm()
|
|
||||||
{
|
function clickAddDm() {
|
||||||
switchRoomContent("title.create.dm", addDmScreen, false);
|
switchRoomContent("title.create.dm", addDmScreen, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1353,12 +1366,20 @@ sidebarAddButton.addEventListener("click", (e) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
document.addEventListener("click", (e) => {
|
||||||
history.pushState({trap: true}, "", location.href);
|
history.pushState({trap: true}, "", location.href);
|
||||||
|
|
||||||
if (fixedContextMenu.classList.contains("show")) {
|
if (fixedContextMenu.classList.contains("show")) {
|
||||||
if (!fixedContextMenu.contains(e.target)) {
|
if (!fixedContextMenu.contains(e.target)) {
|
||||||
fixedContextMenu.classList.remove("show");
|
fixedContextMenu.classList.remove("show");
|
||||||
|
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||||
if (!e.target.closest('sidebarelement')) {
|
if (!e.target.closest('sidebarelement')) {
|
||||||
setActiveSidebarIndicator(currentMainIndicator);
|
setActiveSidebarIndicator(currentMainIndicator);
|
||||||
}
|
}
|
||||||
|
|
@ -1378,16 +1399,15 @@ window.addEventListener("popstate", (e) => {
|
||||||
popstate();
|
popstate();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { App } = window.Capacitor?.Plugins || {};
|
const {App} = window.Capacitor?.Plugins || {};
|
||||||
if (App) {
|
if (App) {
|
||||||
App.addListener('backButton', () => {
|
App.addListener('backButton', () => {
|
||||||
popstate();
|
popstate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function popstate()
|
|
||||||
{
|
function popstate() {
|
||||||
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details'))
|
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
|
||||||
{
|
|
||||||
history.back();
|
history.back();
|
||||||
if (App) {
|
if (App) {
|
||||||
App.minimizeApp();
|
App.minimizeApp();
|
||||||
|
|
@ -1400,6 +1420,7 @@ function popstate()
|
||||||
function gotoSideProfilePopup() {
|
function gotoSideProfilePopup() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActiveRoombarItem(itemId) {
|
function setActiveRoombarItem(itemId) {
|
||||||
let items = document.querySelectorAll('.collapse-text-button');
|
let items = document.querySelectorAll('.collapse-text-button');
|
||||||
items.forEach(item => item.classList.remove('selected'));
|
items.forEach(item => item.classList.remove('selected'));
|
||||||
|
|
@ -1502,7 +1523,7 @@ async function renderInvites(res, type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.cachedInvites = { 'received': null, 'sent': null };
|
window.cachedInvites = {'received': null, 'sent': null};
|
||||||
|
|
||||||
async function loadInvites(tab) {
|
async function loadInvites(tab) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1529,6 +1550,7 @@ async function switchInvitesTab(tab) {
|
||||||
|
|
||||||
await loadInvites(tab);
|
await loadInvites(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(targetId) { //TODO: Implement key generation
|
async function acceptInvite(targetId) { //TODO: Implement key generation
|
||||||
try {
|
try {
|
||||||
showAction("action.invite.accepting", "invite.action");
|
showAction("action.invite.accepting", "invite.action");
|
||||||
|
|
@ -1663,8 +1685,9 @@ function setActiveSidebarIndicator(element, isTemporary = false) {
|
||||||
currentMainIndicator = element;
|
currentMainIndicator = element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mobileNavBack() {
|
function mobileNavBack() {
|
||||||
if(mainScreen.classList.contains('mobile-details')) {
|
if (mainScreen.classList.contains('mobile-details')) {
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
mainScreen.classList.add('mobile-content');
|
mainScreen.classList.add('mobile-content');
|
||||||
|
|
||||||
|
|
@ -1681,6 +1704,7 @@ function mobileNavBack() {
|
||||||
setActiveSidebarIndicator(currentMainIndicator);
|
setActiveSidebarIndicator(currentMainIndicator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mobileNavDetails() {
|
function mobileNavDetails() {
|
||||||
mainScreen.classList.add('mobile-details');
|
mainScreen.classList.add('mobile-details');
|
||||||
}
|
}
|
||||||
|
|
@ -1701,7 +1725,7 @@ document.addEventListener('touchstart', e => {
|
||||||
if (activeTouchButton) {
|
if (activeTouchButton) {
|
||||||
activeTouchButton.classList.add('is-pressed');
|
activeTouchButton.classList.add('is-pressed');
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, {passive: true});
|
||||||
|
|
||||||
document.addEventListener('touchmove', e => {
|
document.addEventListener('touchmove', e => {
|
||||||
if (!touchMoved) {
|
if (!touchMoved) {
|
||||||
|
|
@ -1711,13 +1735,17 @@ document.addEventListener('touchmove', e => {
|
||||||
if (diffX > rem || diffY > rem) {
|
if (diffX > rem || diffY > rem) {
|
||||||
touchMoved = true;
|
touchMoved = true;
|
||||||
|
|
||||||
|
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTouchButton) {
|
if (activeTouchButton) {
|
||||||
activeTouchButton.classList.remove('is-pressed');
|
activeTouchButton.classList.remove('is-pressed');
|
||||||
activeTouchButton = null;
|
activeTouchButton = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, {passive: true});
|
||||||
|
|
||||||
document.addEventListener('touchend', e => {
|
document.addEventListener('touchend', e => {
|
||||||
touchEndX = e.changedTouches[0].screenX;
|
touchEndX = e.changedTouches[0].screenX;
|
||||||
|
|
@ -1757,6 +1785,7 @@ document.addEventListener('contextmenu', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMobileSwipe() {
|
function handleMobileSwipe() {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) return;
|
if (window.innerWidth > 52 * rem) return;
|
||||||
|
|
@ -1785,7 +1814,7 @@ async function renderDms(res) {
|
||||||
let dmEntries = Object.entries(dms);
|
let dmEntries = Object.entries(dms);
|
||||||
|
|
||||||
// Sort by timestamp descending
|
// Sort by timestamp descending
|
||||||
dmEntries.sort((a,b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
|
dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
@ -1842,6 +1871,19 @@ var loadedMessages = {};
|
||||||
var oldestLoadedMsgId = null;
|
var oldestLoadedMsgId = null;
|
||||||
var isLoadingOlderMessages = false;
|
var isLoadingOlderMessages = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
if (!unsafe) return "";
|
||||||
|
return unsafe
|
||||||
|
.replaceAll(/&/g, "&")
|
||||||
|
.replaceAll(/</g, "<")
|
||||||
|
.replaceAll(/>/g, ">")
|
||||||
|
.replaceAll(/"/g, """)
|
||||||
|
.replaceAll(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
async function openDm(dmId, username, targetId) {
|
async function openDm(dmId, username, targetId) {
|
||||||
try {
|
try {
|
||||||
showAction("action.dm.opening", "dmopen");
|
showAction("action.dm.opening", "dmopen");
|
||||||
|
|
@ -1895,7 +1937,7 @@ async function openDm(dmId, username, targetId) {
|
||||||
|
|
||||||
async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
||||||
try {
|
try {
|
||||||
let payload = JSON.stringify({ string1: dmId, string2: "50", string3: startOffset });
|
let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset});
|
||||||
let res = await fetchEncrypted("dm/messages/get", payload);
|
let res = await fetchEncrypted("dm/messages/get", payload);
|
||||||
|
|
||||||
if (res && res.startsWith("error:")) {
|
if (res && res.startsWith("error:")) {
|
||||||
|
|
@ -1927,21 +1969,31 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
let container = document.getElementById("chat-messages");
|
let container = document.getElementById("chat-messages");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
let entries = Object.entries(messages).sort((a,b) => parseInt(a[0]) - parseInt(b[0]));
|
let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
let lastAuthor = null;
|
let lastAuthor = null;
|
||||||
|
|
||||||
for (let [msgId, msg] of entries) {
|
for (let [msgId, msg] of entries) {
|
||||||
let content = msg.content;
|
let content = msg.content;
|
||||||
|
let reactions = msg.reactions || "";
|
||||||
|
let decrypted = false;
|
||||||
if (msg.key && msg.key !== "") {
|
if (msg.key && msg.key !== "") {
|
||||||
try {
|
try {
|
||||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||||
} catch(e) {
|
if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
|
||||||
|
decrypted = true;
|
||||||
|
} catch (e) {
|
||||||
content = `<span style="color:var(--error-color)"><blah>error:messages.decrypt.failed</blah></span>`;
|
content = `<span style="color:var(--error-color)"><blah>error:messages.decrypt.failed</blah></span>`;
|
||||||
|
decrypted = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
decrypted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decrypted) {
|
||||||
|
content = escapeHtml(content);
|
||||||
}
|
}
|
||||||
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
|
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
|
||||||
return processBlah(p1);
|
return processBlah(p1);
|
||||||
|
|
@ -1967,6 +2019,9 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
dmPfpCache[msg.author] = await getAvatarUrl(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]) {
|
if (dmUsernameCache[msg.author]) {
|
||||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||||
pfp = dmPfpCache[msg.author];
|
pfp = dmPfpCache[msg.author];
|
||||||
|
|
@ -1978,15 +2033,129 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
|
|
||||||
let extraClass = showAvatar ? "with-avatar" : "";
|
let extraClass = showAvatar ? "with-avatar" : "";
|
||||||
|
|
||||||
|
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 = "Someone";
|
||||||
|
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 = "Someone";
|
||||||
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${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}')"><svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${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 += `
|
html += `
|
||||||
<div class="chat-message ${extraClass}">
|
<div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
|
||||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||||
<div class="chat-message-content">
|
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')" ontouchstart="handleMessageTouchStart(event, '${msgId}')" ontouchend="handleMessageTouchEnd()" ontouchmove="handleMessageTouchMove()">
|
||||||
${showAvatar ? `<div class="chat-message-header">
|
${showAvatar ? `<div class="chat-message-header">
|
||||||
<span class="chat-message-author">${authorName}</span>
|
<span class="chat-message-author">${authorName}</span>
|
||||||
<span class="chat-message-timestamp">${timeStr}</span>
|
<span class="chat-message-timestamp">${timeStr}</span>
|
||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
|
${repliedHtml}
|
||||||
<div class="chat-message-text">${content}</div>
|
<div class="chat-message-text">${content}</div>
|
||||||
|
${reactionsHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -2013,24 +2182,52 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
ignoreScroll = true;
|
||||||
|
container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ignoreScroll = false;
|
||||||
|
});
|
||||||
|
await delay(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
function setupChatScrollListener() {
|
function setupChatScrollListener() {
|
||||||
let container = document.getElementById("chat-messages");
|
let container = document.getElementById("chat-messages");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.onscroll = async () => {
|
container.onscroll = async () => {
|
||||||
|
|
||||||
|
if (!ignoreScroll) {
|
||||||
|
isAdjusting = false;
|
||||||
|
lastChatScroll = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
|
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
|
||||||
isLoadingOlderMessages = true;
|
isLoadingOlderMessages = true;
|
||||||
showAction("info.messages.loading.older", "messages.loading.older");
|
showAction("info.messages.loading.older", "messages.loading.older");
|
||||||
try {
|
try {
|
||||||
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
||||||
isLoadingOlderMessages = false;
|
isLoadingOlderMessages = false;
|
||||||
|
} catch (e) {
|
||||||
}
|
}
|
||||||
catch (e) {}
|
|
||||||
clearAction("messages.loading.older");
|
clearAction("messages.loading.older");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let replyingToMsgId = null;
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
if (!currentDmId || !currentDmKey) return;
|
if (!currentDmId || !currentDmKey) return;
|
||||||
|
|
||||||
|
|
@ -2038,6 +2235,21 @@ async function sendMessage() {
|
||||||
let content = input.value.trim();
|
let content = input.value.trim();
|
||||||
if (!content) return;
|
if (!content) 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 {
|
try {
|
||||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||||
|
|
@ -2045,23 +2257,33 @@ async function sendMessage() {
|
||||||
let msgPayload = {
|
let msgPayload = {
|
||||||
string1: currentDmId,
|
string1: currentDmId,
|
||||||
string2: encryptedContent,
|
string2: encryptedContent,
|
||||||
string3: ""
|
string3: "",
|
||||||
|
string4: replyingToMsgId || ""
|
||||||
};
|
};
|
||||||
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
input.style.height = '2.5rem';
|
input.style.height = '2.5rem';
|
||||||
|
|
||||||
window.forceScrollToBottom = true;
|
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));
|
let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
|
||||||
|
clearAction("msgsending");
|
||||||
if (res && res.startsWith("error:")) {
|
if (res && res.startsWith("error:")) {
|
||||||
showBlahNotification(res);
|
showBlahNotification(res);
|
||||||
} else {
|
let pElem = document.getElementById(pendingMsgId);
|
||||||
await loadDmMessages(currentDmId);
|
if (pElem) pElem.remove();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
clearAction("msgsending");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showBlahNotification("error:message.send.failed");
|
showBlahNotification("error:message.send.failed");
|
||||||
|
let pElem = document.getElementById(pendingMsgId);
|
||||||
|
if (pElem) pElem.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2098,7 +2320,7 @@ function setupWebSocket() {
|
||||||
try {
|
try {
|
||||||
let nonce = await getNonce(id, passwordHash);
|
let nonce = await getNonce(id, passwordHash);
|
||||||
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
|
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
|
||||||
appWebSocket.send(JSON.stringify({ string1: id, string2: secretEnc }));
|
appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WS auth failed", e);
|
console.error("WS auth failed", e);
|
||||||
}
|
}
|
||||||
|
|
@ -2118,3 +2340,212 @@ function setupWebSocket() {
|
||||||
setTimeout(setupWebSocket, 5000);
|
setTimeout(setupWebSocket, 5000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentContextMenuMsgId = null;
|
||||||
|
let touchTimeout = null;
|
||||||
|
let touchTarget = null;
|
||||||
|
|
||||||
|
function handleMessageContextMenu(e, msgId) {
|
||||||
|
e.preventDefault();
|
||||||
|
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");
|
||||||
|
if (delBtn) {
|
||||||
|
if (loadedMessages[msgId] && loadedMessages[msgId].author !== id) {
|
||||||
|
delBtn.style.display = "none";
|
||||||
|
} else {
|
||||||
|
delBtn.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageTouchStart(e, msgId) {
|
||||||
|
touchTarget = e.target;
|
||||||
|
touchTimeout = setTimeout(() => {
|
||||||
|
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||||
|
currentContextMenuMsgId = msgId;
|
||||||
|
let elem = document.getElementById(`msg-${msgId}`);
|
||||||
|
if (elem) elem.classList.add("context-menu-open");
|
||||||
|
|
||||||
|
let touch = e.touches[0];
|
||||||
|
showFixedContextMenu({
|
||||||
|
top: touch.clientY,
|
||||||
|
right: touch.clientX,
|
||||||
|
bottom: touch.clientY,
|
||||||
|
left: touch.clientX
|
||||||
|
}, messageContextMenu);
|
||||||
|
let delBtn = fixedContextMenu.querySelector("#context-delete-btn");
|
||||||
|
if (delBtn) {
|
||||||
|
if (loadedMessages[msgId] && loadedMessages[msgId].author !== id) {
|
||||||
|
delBtn.style.display = "none";
|
||||||
|
} else {
|
||||||
|
delBtn.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageTouchEnd() {
|
||||||
|
if (touchTimeout) clearTimeout(touchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageTouchMove() {
|
||||||
|
if (touchTimeout) clearTimeout(touchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(msgId) {
|
||||||
|
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||||
|
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.sending.message", "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");
|
||||||
|
|
||||||
|
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 = "Someone";
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reactMessagePrompt(msgId, quickReaction = null) {
|
||||||
|
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||||
|
let reaction = quickReaction;
|
||||||
|
if (!reaction) {
|
||||||
|
reaction = prompt("Enter reaction (emoji or text):");
|
||||||
|
if (!reaction) return;
|
||||||
|
} else {
|
||||||
|
reaction = decodeURIComponent(reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAction("action.message.reacting", "msgreact");
|
||||||
|
try {
|
||||||
|
let currentMsg = loadedMessages[msgId];
|
||||||
|
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 msgPayload = {
|
||||||
|
string1: currentDmId,
|
||||||
|
string2: msgId,
|
||||||
|
string3: encryptedReactions
|
||||||
|
};
|
||||||
|
let res = await fetchEncrypted("dm/message/react", JSON.stringify(msgPayload));
|
||||||
|
if (res && res.startsWith("error:")) {
|
||||||
|
showBlahNotification(res);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showBlahNotification("error:message.react.failed");
|
||||||
|
}
|
||||||
|
clearAction("msgreact");
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,14 @@ var chatScreen = `
|
||||||
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
||||||
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="replying-bar" style="display: none; padding: 0.5rem 1rem; border-top: var(--border-width) solid var(--light-border-color); font-size: 0.85rem; justify-content: space-between; align-items: center; background: var(--main-bg-color);">
|
||||||
|
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;">
|
||||||
|
<span style="opacity: 0.7;"><blah>title.replied.to</blah> </span><strong id="replying-to-name"></strong>: <span id="replying-to-text" style="opacity: 0.7;"></span>
|
||||||
|
</div>
|
||||||
|
<button onclick="cancelReply()" style="background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0.2rem; margin-left: 0.5rem;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" viewBox="0 -960 960 960" fill="currentColor"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
||||||
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
|
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
|
||||||
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
||||||
|
|
@ -220,6 +228,20 @@ var addSpaceMenu = `
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
var messageContextMenu = `
|
||||||
|
<button onclick="replyMessage(currentContextMenuMsgId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/></svg>
|
||||||
|
<span class="blah">title.reply.message</span>
|
||||||
|
</button>
|
||||||
|
<button onclick="reactMessagePrompt(currentContextMenuMsgId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||||
|
<span class="blah">title.react.message</span>
|
||||||
|
</button>
|
||||||
|
<button id="context-delete-btn" onclick="deleteMessage(currentContextMenuMsgId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="1.4rem" viewBox="0 -960 960 960" fill="var(--big-red)"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>
|
||||||
|
<span class="blah" style="color: var(--big-red)">title.delete.message</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
//elements
|
//elements
|
||||||
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
||||||
|
|
|
||||||
|
|
@ -630,7 +630,18 @@ space{
|
||||||
.chat-message {
|
.chat-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0.2rem 0;
|
padding: 0.4rem 0.6rem;
|
||||||
|
margin: 0 -0.6rem;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.chat-message:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chat-message:active, .chat-message.context-menu-open {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
.chat-message.with-avatar {
|
.chat-message.with-avatar {
|
||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@
|
||||||
"title.back.to.register": "back to registration",
|
"title.back.to.register": "back to registration",
|
||||||
"placeholder.username": "cat name",
|
"placeholder.username": "cat name",
|
||||||
"placeholder.captcha.code": "captcha code",
|
"placeholder.captcha.code": "captcha code",
|
||||||
"placeholder.message.input": "meow...",
|
|
||||||
"desc.messages.loading": "loading meows...",
|
"desc.messages.loading": "loading meows...",
|
||||||
"desc.no.dms": "no direct meowchats :c",
|
"desc.no.dms": "no direct meowchats :c",
|
||||||
"action.dm.opening": "opening meowchat...",
|
"action.dm.opening": "opening meowchat...",
|
||||||
|
|
@ -128,5 +127,18 @@
|
||||||
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
|
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
|
||||||
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
|
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
|
||||||
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
||||||
"messages.decrypt.failed": "failed to decrypt meow :c"
|
"messages.decrypt.failed": "failed to decrypt meow :c",
|
||||||
|
"title.reply.message": "meow back",
|
||||||
|
"title.react.message": "purr",
|
||||||
|
"title.delete.message": "hiss away",
|
||||||
|
"title.replied.to": "meowed back to",
|
||||||
|
"action.message.deleting": "hissing message away...",
|
||||||
|
"action.message.sending": "sending meow...",
|
||||||
|
"action.message.reacting": "adding purr...",
|
||||||
|
"error:message.delete.failed": "failed to hiss away meow",
|
||||||
|
"error:message.send.failed": "failed to send meow",
|
||||||
|
"error:message.react.failed": "failed to add purr",
|
||||||
|
"info.sending.message": "sending...",
|
||||||
|
"placeholder.message.input": "meow...",
|
||||||
|
"larp.redacted": "this meow was hissed away."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,5 +127,17 @@
|
||||||
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
|
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
|
||||||
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
|
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
|
||||||
"dm.messages.fetch.failed": "Failed to fetch messages",
|
"dm.messages.fetch.failed": "Failed to fetch messages",
|
||||||
"messages.decrypt.failed": "Failed to decrypt message"
|
"messages.decrypt.failed": "Failed to decrypt message",
|
||||||
|
"title.reply.message": "Reply",
|
||||||
|
"title.react.message": "React",
|
||||||
|
"title.delete.message": "Delete",
|
||||||
|
"title.replied.to": "Replied to",
|
||||||
|
"action.message.deleting": "Deleting message...",
|
||||||
|
"action.message.sending": "Sending message...",
|
||||||
|
"action.message.reacting": "Adding reaction...",
|
||||||
|
"error:message.delete.failed": "Failed to delete message",
|
||||||
|
"error:message.send.failed": "Failed to send message",
|
||||||
|
"error:message.react.failed": "Failed to add reaction",
|
||||||
|
"info.sending.message": "Sending...",
|
||||||
|
"larp.redacted": "This message was deleted."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
577
webroot/main.js
577
webroot/main.js
|
|
@ -1,18 +1,16 @@
|
||||||
var prot = window.location.protocol;
|
var prot = window.location.protocol;
|
||||||
|
|
||||||
async function updateProtocolAndUrl(host)
|
async function updateProtocolAndUrl(host) {
|
||||||
{
|
|
||||||
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
||||||
if (prot == "http:") {
|
if (prot == "http:") {
|
||||||
try {
|
try {
|
||||||
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
try {
|
try {
|
||||||
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
||||||
prot = "https:";
|
prot = "https:";
|
||||||
|
} catch (error) {
|
||||||
}
|
}
|
||||||
catch (error) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
url = `${prot}//${host}/_larpix`;
|
url = `${prot}//${host}/_larpix`;
|
||||||
|
|
@ -148,7 +146,7 @@ async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) {
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
"raw",
|
"raw",
|
||||||
enc.encode(passwordPlain),
|
enc.encode(passwordPlain),
|
||||||
{ name: "PBKDF2" },
|
{name: "PBKDF2"},
|
||||||
false,
|
false,
|
||||||
["deriveKey"]
|
["deriveKey"]
|
||||||
);
|
);
|
||||||
|
|
@ -160,7 +158,7 @@ async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) {
|
||||||
hash: "SHA-256"
|
hash: "SHA-256"
|
||||||
},
|
},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: "AES-GCM", length: 256 },
|
{name: "AES-GCM", length: 256},
|
||||||
false,
|
false,
|
||||||
["encrypt", "decrypt"]
|
["encrypt", "decrypt"]
|
||||||
);
|
);
|
||||||
|
|
@ -172,7 +170,7 @@ async function encryptJsonWithPassword(jsonObj, passwordPlain) {
|
||||||
const iterations = 310000;
|
const iterations = 310000;
|
||||||
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations);
|
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations);
|
||||||
const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj));
|
const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj));
|
||||||
const ciphertextBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
|
const ciphertextBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, plaintext);
|
||||||
const ciphertext = new Uint8Array(ciphertextBuf);
|
const ciphertext = new Uint8Array(ciphertextBuf);
|
||||||
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
|
|
@ -195,7 +193,7 @@ async function decryptJsonWithPassword(envelopeJson, passwordPlain) {
|
||||||
const iv = base64ToUint8(env.iv);
|
const iv = base64ToUint8(env.iv);
|
||||||
const ct = base64ToUint8(env.ct);
|
const ct = base64ToUint8(env.ct);
|
||||||
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter);
|
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter);
|
||||||
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
const plainBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
|
||||||
return JSON.parse(new TextDecoder().decode(plainBuf));
|
return JSON.parse(new TextDecoder().decode(plainBuf));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +207,7 @@ async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length =
|
||||||
const salt = saltBytes ?? new Uint8Array(32);
|
const salt = saltBytes ?? new Uint8Array(32);
|
||||||
const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]);
|
const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]);
|
||||||
const bits = await crypto.subtle.deriveBits(
|
const bits = await crypto.subtle.deriveBits(
|
||||||
{ name: "HKDF", hash: "SHA-256", salt, info },
|
{name: "HKDF", hash: "SHA-256", salt, info},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
length * 8
|
length * 8
|
||||||
);
|
);
|
||||||
|
|
@ -217,14 +215,14 @@ async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length =
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importAesGcmKey(keyBytes) {
|
async function importAesGcmKey(keyBytes) {
|
||||||
return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
return crypto.subtle.importKey("raw", keyBytes, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function encryptAesGcmToBase64(plainText, keyBytes) {
|
async function encryptAesGcmToBase64(plainText, keyBytes) {
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
const key = await importAesGcmKey(keyBytes);
|
const key = await importAesGcmKey(keyBytes);
|
||||||
const pt = new TextEncoder().encode(plainText);
|
const pt = new TextEncoder().encode(plainText);
|
||||||
const ctBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt);
|
const ctBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, pt);
|
||||||
const combined = new Uint8Array(iv.length + ctBuf.byteLength);
|
const combined = new Uint8Array(iv.length + ctBuf.byteLength);
|
||||||
combined.set(iv, 0);
|
combined.set(iv, 0);
|
||||||
combined.set(new Uint8Array(ctBuf), iv.length);
|
combined.set(new Uint8Array(ctBuf), iv.length);
|
||||||
|
|
@ -236,7 +234,7 @@ async function decryptAesGcmFromBase64(cipherBase64, keyBytes) {
|
||||||
const iv = combined.slice(0, 12);
|
const iv = combined.slice(0, 12);
|
||||||
const ct = combined.slice(12);
|
const ct = combined.slice(12);
|
||||||
const key = await importAesGcmKey(keyBytes);
|
const key = await importAesGcmKey(keyBytes);
|
||||||
const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
const ptBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
|
||||||
return new TextDecoder().decode(ptBuf);
|
return new TextDecoder().decode(ptBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -704,7 +702,7 @@ function clearAction(actionid) {
|
||||||
|
|
||||||
notif.addEventListener('transitionend', async () => {
|
notif.addEventListener('transitionend', async () => {
|
||||||
notif.remove();
|
notif.remove();
|
||||||
}, { once: true });
|
}, {once: true});
|
||||||
setTimeout(() => notif.remove(), 300);
|
setTimeout(() => notif.remove(), 300);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -744,6 +742,7 @@ function getInitials(name) {
|
||||||
//1 word at least 2 letters
|
//1 word at least 2 letters
|
||||||
return word.substring(0, 2).toUpperCase();
|
return word.substring(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringToColor(str) {
|
function stringToColor(str) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
|
@ -755,6 +754,7 @@ function stringToColor(str) {
|
||||||
|
|
||||||
return `hsl(${h}, ${s}%, ${l}%)`;
|
return `hsl(${h}, ${s}%, ${l}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAvatarSvg(name, size = 512) {
|
function createAvatarSvg(name, size = 512) {
|
||||||
const initials = getInitials(name);
|
const initials = getInitials(name);
|
||||||
const color = stringToColor(name);
|
const color = stringToColor(name);
|
||||||
|
|
@ -777,17 +777,14 @@ function createAvatarSvg(name, size = 512) {
|
||||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvatarUrl(id, username)
|
async function getAvatarUrl(id, username) {
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
|
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
|
||||||
if ((await fetchAsync(pfpUrl)) == "")
|
if ((await fetchAsync(pfpUrl)) == "") {
|
||||||
{
|
|
||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
return pfpUrl;
|
return pfpUrl;
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
return createAvatarSvg(username);
|
return createAvatarSvg(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -810,6 +807,7 @@ async function loadingFadeOut() {
|
||||||
mainScreen.style.transform = "";
|
mainScreen.style.transform = "";
|
||||||
mainScreen.style.opacity = "";
|
mainScreen.style.opacity = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadingFadeIn() {
|
async function loadingFadeIn() {
|
||||||
|
|
||||||
loadingScreen.style.transform = "scale(0.85)";
|
loadingScreen.style.transform = "scale(0.85)";
|
||||||
|
|
@ -852,8 +850,7 @@ async function initBlahs() {
|
||||||
|
|
||||||
let placeholders = document.querySelectorAll("[placeholder]");
|
let placeholders = document.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -898,7 +895,7 @@ function processBlah(blahmessage) {
|
||||||
let valueslist = "";
|
let valueslist = "";
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
let value = processBlah(values[i]);
|
let value = processBlah(values[i]);
|
||||||
valueslist+=`${value}, `;
|
valueslist += `${value}, `;
|
||||||
message = message.replaceAll(`{${i}}`, value);
|
message = message.replaceAll(`{${i}}`, value);
|
||||||
}
|
}
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
|
|
@ -908,9 +905,7 @@ function processBlah(blahmessage) {
|
||||||
message = message.replaceAll('{all}', valueslist);
|
message = message.replaceAll('{all}', valueslist);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
} catch (e) {
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
if (prepended) {
|
if (prepended) {
|
||||||
return blahmessage.substring(1);
|
return blahmessage.substring(1);
|
||||||
}
|
}
|
||||||
|
|
@ -962,7 +957,7 @@ async function generateUserKeysBundle() {
|
||||||
privX25519: uint8ToBase64(privX25519),
|
privX25519: uint8ToBase64(privX25519),
|
||||||
privMlKem: uint8ToBase64(privMlKem)
|
privMlKem: uint8ToBase64(privMlKem)
|
||||||
};
|
};
|
||||||
return { pub, priv };
|
return {pub, priv};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureUserKeys() {
|
async function ensureUserKeys() {
|
||||||
|
|
@ -1082,6 +1077,7 @@ async function mainJS() {
|
||||||
|
|
||||||
await start();
|
await start();
|
||||||
}
|
}
|
||||||
|
|
||||||
id = localStorage.getItem('id');
|
id = localStorage.getItem('id');
|
||||||
username = localStorage.getItem('username');
|
username = localStorage.getItem('username');
|
||||||
password = localStorage.getItem('password');
|
password = localStorage.getItem('password');
|
||||||
|
|
@ -1104,8 +1100,7 @@ function showFixedContextMenu(rect, html) {
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1114,10 +1109,31 @@ function showFixedContextMenu(rect, html) {
|
||||||
|
|
||||||
fixedContextMenu.innerHTML = doc.body.innerHTML;
|
fixedContextMenu.innerHTML = doc.body.innerHTML;
|
||||||
|
|
||||||
fixedContextMenu.style.left = `${rect.right + 10}px`;
|
fixedContextMenu.style.transition = 'none';
|
||||||
fixedContextMenu.style.top = `${rect.top}px`;
|
fixedContextMenu.style.opacity = '0';
|
||||||
|
fixedContextMenu.style.left = '0px';
|
||||||
|
fixedContextMenu.style.top = '0px';
|
||||||
|
|
||||||
fixedContextMenu.classList.add("show");
|
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 currentRoomsBarTitle = null;
|
||||||
|
|
@ -1156,8 +1172,7 @@ async function switchRoomsBar(title, content) {
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1171,10 +1186,11 @@ async function switchRoomsBar(title, content) {
|
||||||
roomsTopBarTransition.style.transform = "";
|
roomsTopBarTransition.style.transform = "";
|
||||||
roomsTopBarTransition.style.opacity = "";
|
roomsTopBarTransition.style.opacity = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentRoomContentTitle = null;
|
var currentRoomContentTitle = null;
|
||||||
var currentRoomContentHtml = null;
|
var currentRoomContentHtml = null;
|
||||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false)
|
|
||||||
{
|
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
||||||
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
||||||
|
|
@ -1203,9 +1219,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
mainScreen.classList.add('mobile-content');
|
mainScreen.classList.add('mobile-content');
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
await delay(200);
|
await delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1229,8 +1243,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1246,10 +1259,11 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentDetailsContentTitle = null;
|
var currentDetailsContentTitle = null;
|
||||||
var currentDetailsContentHtml = null;
|
var currentDetailsContentHtml = null;
|
||||||
async function switchDetailsContent(title, content)
|
|
||||||
{
|
async function switchDetailsContent(title, content) {
|
||||||
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
|
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
|
||||||
currentDetailsContentTitle = title;
|
currentDetailsContentTitle = title;
|
||||||
currentDetailsContentHtml = content;
|
currentDetailsContentHtml = content;
|
||||||
|
|
@ -1284,8 +1298,7 @@ async function switchDetailsContent(title, content)
|
||||||
|
|
||||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||||
for (let i = 0; i < placeholders.length; i++) {
|
for (let i = 0; i < placeholders.length; i++) {
|
||||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||||
{
|
|
||||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||||
placeholders[i].placeholder = processBlah(value);
|
placeholders[i].placeholder = processBlah(value);
|
||||||
}
|
}
|
||||||
|
|
@ -1301,8 +1314,8 @@ async function switchDetailsContent(title, content)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
function clickCollapseDms()
|
|
||||||
{
|
function clickCollapseDms() {
|
||||||
var collapseDmsBtn = document.getElementById("collapse-dms");
|
var collapseDmsBtn = document.getElementById("collapse-dms");
|
||||||
collapseDmsBtn.classList.toggle("collapsed");
|
collapseDmsBtn.classList.toggle("collapsed");
|
||||||
var dmsWrapper = document.getElementById("dms-wrapper");
|
var dmsWrapper = document.getElementById("dms-wrapper");
|
||||||
|
|
@ -1314,8 +1327,8 @@ function clickCollapseDms()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clickCollapseGroups()
|
|
||||||
{
|
function clickCollapseGroups() {
|
||||||
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
||||||
collapseGroupsBtn.classList.toggle("collapsed");
|
collapseGroupsBtn.classList.toggle("collapsed");
|
||||||
var groupsWrapper = document.getElementById("groups-wrapper");
|
var groupsWrapper = document.getElementById("groups-wrapper");
|
||||||
|
|
@ -1327,12 +1340,12 @@ function clickCollapseGroups()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clickAddGroup()
|
|
||||||
{
|
function clickAddGroup() {
|
||||||
switchRoomContent("title.create.group", createGroupScreen, false);
|
switchRoomContent("title.create.group", createGroupScreen, false);
|
||||||
}
|
}
|
||||||
function clickAddDm()
|
|
||||||
{
|
function clickAddDm() {
|
||||||
switchRoomContent("title.create.dm", addDmScreen, false);
|
switchRoomContent("title.create.dm", addDmScreen, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1353,12 +1366,20 @@ sidebarAddButton.addEventListener("click", (e) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
document.addEventListener("click", (e) => {
|
||||||
history.pushState({trap: true}, "", location.href);
|
history.pushState({trap: true}, "", location.href);
|
||||||
|
|
||||||
if (fixedContextMenu.classList.contains("show")) {
|
if (fixedContextMenu.classList.contains("show")) {
|
||||||
if (!fixedContextMenu.contains(e.target)) {
|
if (!fixedContextMenu.contains(e.target)) {
|
||||||
fixedContextMenu.classList.remove("show");
|
fixedContextMenu.classList.remove("show");
|
||||||
|
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||||
if (!e.target.closest('sidebarelement')) {
|
if (!e.target.closest('sidebarelement')) {
|
||||||
setActiveSidebarIndicator(currentMainIndicator);
|
setActiveSidebarIndicator(currentMainIndicator);
|
||||||
}
|
}
|
||||||
|
|
@ -1378,16 +1399,15 @@ window.addEventListener("popstate", (e) => {
|
||||||
popstate();
|
popstate();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { App } = window.Capacitor?.Plugins || {};
|
const {App} = window.Capacitor?.Plugins || {};
|
||||||
if (App) {
|
if (App) {
|
||||||
App.addListener('backButton', () => {
|
App.addListener('backButton', () => {
|
||||||
popstate();
|
popstate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function popstate()
|
|
||||||
{
|
function popstate() {
|
||||||
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details'))
|
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
|
||||||
{
|
|
||||||
history.back();
|
history.back();
|
||||||
if (App) {
|
if (App) {
|
||||||
App.minimizeApp();
|
App.minimizeApp();
|
||||||
|
|
@ -1400,6 +1420,7 @@ function popstate()
|
||||||
function gotoSideProfilePopup() {
|
function gotoSideProfilePopup() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActiveRoombarItem(itemId) {
|
function setActiveRoombarItem(itemId) {
|
||||||
let items = document.querySelectorAll('.collapse-text-button');
|
let items = document.querySelectorAll('.collapse-text-button');
|
||||||
items.forEach(item => item.classList.remove('selected'));
|
items.forEach(item => item.classList.remove('selected'));
|
||||||
|
|
@ -1502,7 +1523,7 @@ async function renderInvites(res, type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.cachedInvites = { 'received': null, 'sent': null };
|
window.cachedInvites = {'received': null, 'sent': null};
|
||||||
|
|
||||||
async function loadInvites(tab) {
|
async function loadInvites(tab) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1529,6 +1550,7 @@ async function switchInvitesTab(tab) {
|
||||||
|
|
||||||
await loadInvites(tab);
|
await loadInvites(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(targetId) { //TODO: Implement key generation
|
async function acceptInvite(targetId) { //TODO: Implement key generation
|
||||||
try {
|
try {
|
||||||
showAction("action.invite.accepting", "invite.action");
|
showAction("action.invite.accepting", "invite.action");
|
||||||
|
|
@ -1663,8 +1685,9 @@ function setActiveSidebarIndicator(element, isTemporary = false) {
|
||||||
currentMainIndicator = element;
|
currentMainIndicator = element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mobileNavBack() {
|
function mobileNavBack() {
|
||||||
if(mainScreen.classList.contains('mobile-details')) {
|
if (mainScreen.classList.contains('mobile-details')) {
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
mainScreen.classList.add('mobile-content');
|
mainScreen.classList.add('mobile-content');
|
||||||
|
|
||||||
|
|
@ -1681,6 +1704,7 @@ function mobileNavBack() {
|
||||||
setActiveSidebarIndicator(currentMainIndicator);
|
setActiveSidebarIndicator(currentMainIndicator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mobileNavDetails() {
|
function mobileNavDetails() {
|
||||||
mainScreen.classList.add('mobile-details');
|
mainScreen.classList.add('mobile-details');
|
||||||
}
|
}
|
||||||
|
|
@ -1701,7 +1725,7 @@ document.addEventListener('touchstart', e => {
|
||||||
if (activeTouchButton) {
|
if (activeTouchButton) {
|
||||||
activeTouchButton.classList.add('is-pressed');
|
activeTouchButton.classList.add('is-pressed');
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, {passive: true});
|
||||||
|
|
||||||
document.addEventListener('touchmove', e => {
|
document.addEventListener('touchmove', e => {
|
||||||
if (!touchMoved) {
|
if (!touchMoved) {
|
||||||
|
|
@ -1711,13 +1735,17 @@ document.addEventListener('touchmove', e => {
|
||||||
if (diffX > rem || diffY > rem) {
|
if (diffX > rem || diffY > rem) {
|
||||||
touchMoved = true;
|
touchMoved = true;
|
||||||
|
|
||||||
|
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTouchButton) {
|
if (activeTouchButton) {
|
||||||
activeTouchButton.classList.remove('is-pressed');
|
activeTouchButton.classList.remove('is-pressed');
|
||||||
activeTouchButton = null;
|
activeTouchButton = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, {passive: true});
|
||||||
|
|
||||||
document.addEventListener('touchend', e => {
|
document.addEventListener('touchend', e => {
|
||||||
touchEndX = e.changedTouches[0].screenX;
|
touchEndX = e.changedTouches[0].screenX;
|
||||||
|
|
@ -1757,6 +1785,7 @@ document.addEventListener('contextmenu', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMobileSwipe() {
|
function handleMobileSwipe() {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) return;
|
if (window.innerWidth > 52 * rem) return;
|
||||||
|
|
@ -1785,7 +1814,7 @@ async function renderDms(res) {
|
||||||
let dmEntries = Object.entries(dms);
|
let dmEntries = Object.entries(dms);
|
||||||
|
|
||||||
// Sort by timestamp descending
|
// Sort by timestamp descending
|
||||||
dmEntries.sort((a,b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
|
dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
@ -1842,6 +1871,19 @@ var loadedMessages = {};
|
||||||
var oldestLoadedMsgId = null;
|
var oldestLoadedMsgId = null;
|
||||||
var isLoadingOlderMessages = false;
|
var isLoadingOlderMessages = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
if (!unsafe) return "";
|
||||||
|
return unsafe
|
||||||
|
.replaceAll(/&/g, "&")
|
||||||
|
.replaceAll(/</g, "<")
|
||||||
|
.replaceAll(/>/g, ">")
|
||||||
|
.replaceAll(/"/g, """)
|
||||||
|
.replaceAll(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
async function openDm(dmId, username, targetId) {
|
async function openDm(dmId, username, targetId) {
|
||||||
try {
|
try {
|
||||||
showAction("action.dm.opening", "dmopen");
|
showAction("action.dm.opening", "dmopen");
|
||||||
|
|
@ -1895,7 +1937,7 @@ async function openDm(dmId, username, targetId) {
|
||||||
|
|
||||||
async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
||||||
try {
|
try {
|
||||||
let payload = JSON.stringify({ string1: dmId, string2: "50", string3: startOffset });
|
let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset});
|
||||||
let res = await fetchEncrypted("dm/messages/get", payload);
|
let res = await fetchEncrypted("dm/messages/get", payload);
|
||||||
|
|
||||||
if (res && res.startsWith("error:")) {
|
if (res && res.startsWith("error:")) {
|
||||||
|
|
@ -1927,21 +1969,31 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
let container = document.getElementById("chat-messages");
|
let container = document.getElementById("chat-messages");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
let entries = Object.entries(messages).sort((a,b) => parseInt(a[0]) - parseInt(b[0]));
|
let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
let lastAuthor = null;
|
let lastAuthor = null;
|
||||||
|
|
||||||
for (let [msgId, msg] of entries) {
|
for (let [msgId, msg] of entries) {
|
||||||
let content = msg.content;
|
let content = msg.content;
|
||||||
|
let reactions = msg.reactions || "";
|
||||||
|
let decrypted = false;
|
||||||
if (msg.key && msg.key !== "") {
|
if (msg.key && msg.key !== "") {
|
||||||
try {
|
try {
|
||||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||||
} catch(e) {
|
if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
|
||||||
|
decrypted = true;
|
||||||
|
} catch (e) {
|
||||||
content = `<span style="color:var(--error-color)"><blah>error:messages.decrypt.failed</blah></span>`;
|
content = `<span style="color:var(--error-color)"><blah>error:messages.decrypt.failed</blah></span>`;
|
||||||
|
decrypted = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
decrypted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decrypted) {
|
||||||
|
content = escapeHtml(content);
|
||||||
}
|
}
|
||||||
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
|
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
|
||||||
return processBlah(p1);
|
return processBlah(p1);
|
||||||
|
|
@ -1967,6 +2019,9 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
dmPfpCache[msg.author] = await getAvatarUrl(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]) {
|
if (dmUsernameCache[msg.author]) {
|
||||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||||
pfp = dmPfpCache[msg.author];
|
pfp = dmPfpCache[msg.author];
|
||||||
|
|
@ -1978,15 +2033,129 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
|
|
||||||
let extraClass = showAvatar ? "with-avatar" : "";
|
let extraClass = showAvatar ? "with-avatar" : "";
|
||||||
|
|
||||||
|
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 = "Someone";
|
||||||
|
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 = "Someone";
|
||||||
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${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}')"><svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${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 += `
|
html += `
|
||||||
<div class="chat-message ${extraClass}">
|
<div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
|
||||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||||
<div class="chat-message-content">
|
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')" ontouchstart="handleMessageTouchStart(event, '${msgId}')" ontouchend="handleMessageTouchEnd()" ontouchmove="handleMessageTouchMove()">
|
||||||
${showAvatar ? `<div class="chat-message-header">
|
${showAvatar ? `<div class="chat-message-header">
|
||||||
<span class="chat-message-author">${authorName}</span>
|
<span class="chat-message-author">${authorName}</span>
|
||||||
<span class="chat-message-timestamp">${timeStr}</span>
|
<span class="chat-message-timestamp">${timeStr}</span>
|
||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
|
${repliedHtml}
|
||||||
<div class="chat-message-text">${content}</div>
|
<div class="chat-message-text">${content}</div>
|
||||||
|
${reactionsHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -2013,24 +2182,52 @@ async function renderMessages(messages, isPrepend = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
ignoreScroll = true;
|
||||||
|
container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ignoreScroll = false;
|
||||||
|
});
|
||||||
|
await delay(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
function setupChatScrollListener() {
|
function setupChatScrollListener() {
|
||||||
let container = document.getElementById("chat-messages");
|
let container = document.getElementById("chat-messages");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.onscroll = async () => {
|
container.onscroll = async () => {
|
||||||
|
|
||||||
|
if (!ignoreScroll) {
|
||||||
|
isAdjusting = false;
|
||||||
|
lastChatScroll = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
|
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
|
||||||
isLoadingOlderMessages = true;
|
isLoadingOlderMessages = true;
|
||||||
showAction("info.messages.loading.older", "messages.loading.older");
|
showAction("info.messages.loading.older", "messages.loading.older");
|
||||||
try {
|
try {
|
||||||
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
||||||
isLoadingOlderMessages = false;
|
isLoadingOlderMessages = false;
|
||||||
|
} catch (e) {
|
||||||
}
|
}
|
||||||
catch (e) {}
|
|
||||||
clearAction("messages.loading.older");
|
clearAction("messages.loading.older");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let replyingToMsgId = null;
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
if (!currentDmId || !currentDmKey) return;
|
if (!currentDmId || !currentDmKey) return;
|
||||||
|
|
||||||
|
|
@ -2038,6 +2235,21 @@ async function sendMessage() {
|
||||||
let content = input.value.trim();
|
let content = input.value.trim();
|
||||||
if (!content) return;
|
if (!content) 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 {
|
try {
|
||||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||||
|
|
@ -2045,23 +2257,33 @@ async function sendMessage() {
|
||||||
let msgPayload = {
|
let msgPayload = {
|
||||||
string1: currentDmId,
|
string1: currentDmId,
|
||||||
string2: encryptedContent,
|
string2: encryptedContent,
|
||||||
string3: ""
|
string3: "",
|
||||||
|
string4: replyingToMsgId || ""
|
||||||
};
|
};
|
||||||
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
input.style.height = '2.5rem';
|
input.style.height = '2.5rem';
|
||||||
|
|
||||||
window.forceScrollToBottom = true;
|
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));
|
let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
|
||||||
|
clearAction("msgsending");
|
||||||
if (res && res.startsWith("error:")) {
|
if (res && res.startsWith("error:")) {
|
||||||
showBlahNotification(res);
|
showBlahNotification(res);
|
||||||
} else {
|
let pElem = document.getElementById(pendingMsgId);
|
||||||
await loadDmMessages(currentDmId);
|
if (pElem) pElem.remove();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
clearAction("msgsending");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showBlahNotification("error:message.send.failed");
|
showBlahNotification("error:message.send.failed");
|
||||||
|
let pElem = document.getElementById(pendingMsgId);
|
||||||
|
if (pElem) pElem.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2098,7 +2320,7 @@ function setupWebSocket() {
|
||||||
try {
|
try {
|
||||||
let nonce = await getNonce(id, passwordHash);
|
let nonce = await getNonce(id, passwordHash);
|
||||||
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
|
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
|
||||||
appWebSocket.send(JSON.stringify({ string1: id, string2: secretEnc }));
|
appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WS auth failed", e);
|
console.error("WS auth failed", e);
|
||||||
}
|
}
|
||||||
|
|
@ -2118,3 +2340,212 @@ function setupWebSocket() {
|
||||||
setTimeout(setupWebSocket, 5000);
|
setTimeout(setupWebSocket, 5000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentContextMenuMsgId = null;
|
||||||
|
let touchTimeout = null;
|
||||||
|
let touchTarget = null;
|
||||||
|
|
||||||
|
function handleMessageContextMenu(e, msgId) {
|
||||||
|
e.preventDefault();
|
||||||
|
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");
|
||||||
|
if (delBtn) {
|
||||||
|
if (loadedMessages[msgId] && loadedMessages[msgId].author !== id) {
|
||||||
|
delBtn.style.display = "none";
|
||||||
|
} else {
|
||||||
|
delBtn.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageTouchStart(e, msgId) {
|
||||||
|
touchTarget = e.target;
|
||||||
|
touchTimeout = setTimeout(() => {
|
||||||
|
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||||
|
currentContextMenuMsgId = msgId;
|
||||||
|
let elem = document.getElementById(`msg-${msgId}`);
|
||||||
|
if (elem) elem.classList.add("context-menu-open");
|
||||||
|
|
||||||
|
let touch = e.touches[0];
|
||||||
|
showFixedContextMenu({
|
||||||
|
top: touch.clientY,
|
||||||
|
right: touch.clientX,
|
||||||
|
bottom: touch.clientY,
|
||||||
|
left: touch.clientX
|
||||||
|
}, messageContextMenu);
|
||||||
|
let delBtn = fixedContextMenu.querySelector("#context-delete-btn");
|
||||||
|
if (delBtn) {
|
||||||
|
if (loadedMessages[msgId] && loadedMessages[msgId].author !== id) {
|
||||||
|
delBtn.style.display = "none";
|
||||||
|
} else {
|
||||||
|
delBtn.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageTouchEnd() {
|
||||||
|
if (touchTimeout) clearTimeout(touchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageTouchMove() {
|
||||||
|
if (touchTimeout) clearTimeout(touchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(msgId) {
|
||||||
|
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||||
|
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.sending.message", "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");
|
||||||
|
|
||||||
|
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 = "Someone";
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reactMessagePrompt(msgId, quickReaction = null) {
|
||||||
|
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||||
|
let reaction = quickReaction;
|
||||||
|
if (!reaction) {
|
||||||
|
reaction = prompt("Enter reaction (emoji or text):");
|
||||||
|
if (!reaction) return;
|
||||||
|
} else {
|
||||||
|
reaction = decodeURIComponent(reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAction("action.message.reacting", "msgreact");
|
||||||
|
try {
|
||||||
|
let currentMsg = loadedMessages[msgId];
|
||||||
|
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 msgPayload = {
|
||||||
|
string1: currentDmId,
|
||||||
|
string2: msgId,
|
||||||
|
string3: encryptedReactions
|
||||||
|
};
|
||||||
|
let res = await fetchEncrypted("dm/message/react", JSON.stringify(msgPayload));
|
||||||
|
if (res && res.startsWith("error:")) {
|
||||||
|
showBlahNotification(res);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showBlahNotification("error:message.react.failed");
|
||||||
|
}
|
||||||
|
clearAction("msgreact");
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,14 @@ var chatScreen = `
|
||||||
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
||||||
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="replying-bar" style="display: none; padding: 0.5rem 1rem; border-top: var(--border-width) solid var(--light-border-color); font-size: 0.85rem; justify-content: space-between; align-items: center; background: var(--main-bg-color);">
|
||||||
|
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;">
|
||||||
|
<span style="opacity: 0.7;"><blah>title.replied.to</blah> </span><strong id="replying-to-name"></strong>: <span id="replying-to-text" style="opacity: 0.7;"></span>
|
||||||
|
</div>
|
||||||
|
<button onclick="cancelReply()" style="background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0.2rem; margin-left: 0.5rem;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" viewBox="0 -960 960 960" fill="currentColor"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
||||||
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
|
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
|
||||||
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
||||||
|
|
@ -220,6 +228,20 @@ var addSpaceMenu = `
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
var messageContextMenu = `
|
||||||
|
<button onclick="replyMessage(currentContextMenuMsgId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/></svg>
|
||||||
|
<span class="blah">title.reply.message</span>
|
||||||
|
</button>
|
||||||
|
<button onclick="reactMessagePrompt(currentContextMenuMsgId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||||
|
<span class="blah">title.react.message</span>
|
||||||
|
</button>
|
||||||
|
<button id="context-delete-btn" onclick="deleteMessage(currentContextMenuMsgId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="1.4rem" viewBox="0 -960 960 960" fill="var(--big-red)"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>
|
||||||
|
<span class="blah" style="color: var(--big-red)">title.delete.message</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
//elements
|
//elements
|
||||||
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
||||||
|
|
|
||||||
|
|
@ -630,7 +630,18 @@ space{
|
||||||
.chat-message {
|
.chat-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0.2rem 0;
|
padding: 0.4rem 0.6rem;
|
||||||
|
margin: 0 -0.6rem;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.chat-message:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chat-message:active, .chat-message.context-menu-open {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
.chat-message.with-avatar {
|
.chat-message.with-avatar {
|
||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue