add groups (TEST)
This commit is contained in:
parent
afd6a87228
commit
903aedc8ce
14 changed files with 3104 additions and 1402 deletions
|
|
@ -10,6 +10,8 @@
|
|||
"dm.accepted": "direct meowchat accepted",
|
||||
"no.invite.found": "you can't create a dm without invitation *hiss*",
|
||||
"unknown.error": "unknown error :o",
|
||||
"missing.keys": "cannot rotate nametags: some kitties don't have public nametags available yet. they must set up their identity.",
|
||||
"key.rotated": "meowchat keys have been successfully rotated! meow!",
|
||||
"forbidden": "you do not have pawmission to perform this action *hiss*",
|
||||
"dm.key.too.large": "meowchat key too large :o",
|
||||
"dm.key.not.found": "meowchat key not found :c",
|
||||
|
|
@ -125,6 +127,7 @@
|
|||
"desc.messages.loading": "loading meows...",
|
||||
"desc.no.dms": "no direct meowchats :c",
|
||||
"action.dm.opening": "opening meowchat...",
|
||||
"action.group.opening": "opening clowder...",
|
||||
"dm.open.failed": "failed to open meowchat :c",
|
||||
"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",
|
||||
|
|
@ -201,5 +204,58 @@
|
|||
"message.unpinned.by": "{0} unpinned a {1}.",
|
||||
"title.pinned.messages": "Pinned",
|
||||
"error.pinned.messages.empty": "No pinned meows",
|
||||
"title.message.lowercase": "meowsage"
|
||||
"title.message.lowercase": "meowsage",
|
||||
"action.rotate.keys": "Meowtate Keys",
|
||||
"already.in.group": "Meowready in group",
|
||||
"desc.key.rotation.info": "Meow need to purrotate meowcription keys to purrtinue sending meowsages in this group. Other meowmbers will autopurrtically receive the new key.",
|
||||
"desc.no.groups": "Meow are not in any groups yet.",
|
||||
"dm.read": "Purread",
|
||||
"empty.body": "Empty meowdy",
|
||||
"failed.create.group": "Failed to purreate group",
|
||||
"federation.not.implemented": "Meowderation not implemeowted",
|
||||
"group.not.found": "Group not meownd",
|
||||
"group.read": "Purread",
|
||||
"invalid.entry.name": "Invalid purrtry name",
|
||||
"invalid.key.index": "Invalid key index (purrlease purrefresh the page)",
|
||||
"invalid.msg.id": "Invalid meowsage ID",
|
||||
"invalid.payload": "Invalid purrload",
|
||||
"invalid.reaction.id": "Invalid purreaction ID",
|
||||
"invalid.user": "Invalid meowser",
|
||||
"joined": "Purroined",
|
||||
"key.rotation.required": "Key purrotation meowquired",
|
||||
"key.too.large": "Key too meowrge",
|
||||
"keys.public.mismatch": "Public keys mismemeow",
|
||||
"label.members": "MEOWMBERS",
|
||||
"message.deleted": "Meowsage purrleted",
|
||||
"message.not.found": "Meowsage not meownd",
|
||||
"message.pin.toggled": "Meowsage purrin purrggled",
|
||||
"message.reacted": "Meowsage purreacted",
|
||||
"message.reaction.removed": "Purreaction remeowed",
|
||||
"no.invite": "No meowvite",
|
||||
"not.a.member": "Not a meowmber",
|
||||
"not.in.group": "Not in purroup",
|
||||
"reaction.not.found": "Purreaction not meownd",
|
||||
"rotation.not.required": "Purrotation not meowquired",
|
||||
"storage.entry.updated": "Storage purrtry updated",
|
||||
"storage.limit.exceeded": "Storage limit exmeowded",
|
||||
"title.key.rotation": "Key Purrotation",
|
||||
"error:messages.decrypt.failed": "Depurryption meowled",
|
||||
"error.message.already.deleted": "Meowsage alpurrdy meowleted",
|
||||
"group.user.joined.by": "{0} purroined the group",
|
||||
"group.created.by": "Group purreated by {0}",
|
||||
"group.user.left.by": "{0} left the purroup",
|
||||
"action.group.fetch": "Meowtching groups",
|
||||
"group.created": "Group purreated",
|
||||
"group.user.joined": "Purroined group",
|
||||
"prompt.invite.username": "Enter username to invite meow:",
|
||||
"desc.failed.load.members": "Failed to load members meow",
|
||||
"prompt.are.you.sure": "Are you sure meow?",
|
||||
"title.unknown.group": "Unknown Group meow",
|
||||
"title.unknown": "Unknown meow",
|
||||
"en-us": "English meow",
|
||||
"pl-pl": "Polish meow",
|
||||
"en-cat": "Meowlish",
|
||||
"avatar.updated": "Avatar updated successfully",
|
||||
"profile.updated": "Profile updated successfully",
|
||||
"banner.updated": "Banner updated successfully"
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
"dm.accepted": "DM accepted",
|
||||
"no.invite.found": "You can't create a dm without invitation",
|
||||
"unknown.error": "Unknown error",
|
||||
"missing.keys": "Cannot rotate keys: Some members don't have public keys available yet. They must configure their identity.",
|
||||
"key.rotated": "Group keys have been successfully rotated.",
|
||||
"forbidden": "You do not have permission to perform this action",
|
||||
"dm.key.too.large": "DM key too large",
|
||||
"dm.key.not.found": "DM key not found",
|
||||
|
|
@ -124,6 +126,7 @@
|
|||
"desc.messages.loading": "Fetching messages...",
|
||||
"desc.no.dms": "No direct messages",
|
||||
"action.dm.opening": "Opening DM...",
|
||||
"action.group.opening": "Opening group...",
|
||||
"dm.open.failed": "Failed to open DM",
|
||||
"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.",
|
||||
|
|
@ -201,5 +204,58 @@
|
|||
"message.unpinned.by": "{0} unpinned a {1}.",
|
||||
"title.pinned.messages": "Pinned",
|
||||
"error.pinned.messages.empty": "No pinned messages",
|
||||
"title.message.lowercase": "message"
|
||||
"title.message.lowercase": "message",
|
||||
"action.rotate.keys": "Rotate Keys",
|
||||
"already.in.group": "Already in group",
|
||||
"desc.key.rotation.info": "You need to rotate encryption keys to continue sending messages in this group. Other members will automatically receive the new key.",
|
||||
"desc.no.groups": "You are not in any groups yet.",
|
||||
"dm.read": "Read",
|
||||
"empty.body": "Empty body",
|
||||
"failed.create.group": "Failed to create group",
|
||||
"federation.not.implemented": "Federation not implemented",
|
||||
"group.not.found": "Group not found",
|
||||
"group.read": "Read",
|
||||
"invalid.entry.name": "Invalid entry name",
|
||||
"invalid.key.index": "Invalid key index (please refresh the page)",
|
||||
"invalid.msg.id": "Invalid message ID",
|
||||
"invalid.payload": "Invalid payload",
|
||||
"invalid.reaction.id": "Invalid reaction ID",
|
||||
"invalid.user": "Invalid user",
|
||||
"joined": "Joined",
|
||||
"key.rotation.required": "Key rotation required",
|
||||
"key.too.large": "Key too large",
|
||||
"keys.public.mismatch": "Public keys mismatch",
|
||||
"label.members": "MEMBERS",
|
||||
"message.deleted": "Message deleted",
|
||||
"message.not.found": "Message not found",
|
||||
"message.pin.toggled": "Message pin toggled",
|
||||
"message.reacted": "Message reacted",
|
||||
"message.reaction.removed": "Reaction removed",
|
||||
"no.invite": "No invite",
|
||||
"not.a.member": "Not a member",
|
||||
"not.in.group": "Not in group",
|
||||
"reaction.not.found": "Reaction not found",
|
||||
"rotation.not.required": "Rotation not required",
|
||||
"storage.entry.updated": "Storage entry updated",
|
||||
"storage.limit.exceeded": "Storage limit exceeded",
|
||||
"title.key.rotation": "Key Rotation",
|
||||
"error:messages.decrypt.failed": "Decryption failed",
|
||||
"error.message.already.deleted": "Message already deleted",
|
||||
"group.user.joined.by": "{0} joined the group",
|
||||
"group.created.by": "Group created by {0}",
|
||||
"group.user.left.by": "{0} left the group",
|
||||
"action.group.fetch": "Fetching groups",
|
||||
"group.created": "Group created",
|
||||
"group.user.joined": "Joined group",
|
||||
"prompt.invite.username": "Enter username to invite:",
|
||||
"desc.failed.load.members": "Failed to load members",
|
||||
"prompt.are.you.sure": "Are you sure?",
|
||||
"title.unknown.group": "Unknown Group",
|
||||
"title.unknown": "Unknown",
|
||||
"success:banner.updated": "Banner updated successfully",
|
||||
"success:profile.updated": "Profile updated successfully",
|
||||
"success:avatar.updated": "Avatar updated successfully",
|
||||
"en-us": "English",
|
||||
"pl-pl": "Polish",
|
||||
"en-cat": "Meowlish"
|
||||
}
|
||||
|
|
@ -124,6 +124,7 @@
|
|||
"desc.messages.loading": "Pobieranie wiadomości...",
|
||||
"desc.no.dms": "Brak wiadomości prywatnych",
|
||||
"action.dm.opening": "Otwieranie rozmowy...",
|
||||
"action.group.opening": "Otwieranie grupy...",
|
||||
"dm.open.failed": "Nie udało się otworzyć rozmowy",
|
||||
"keys.local.server.mismatch": "To urządzenie ma inne klucze szyfrowania niż serwer. Użyj urządzenia, na którym zalogowano się po raz pierwszy, lub przywróć klucze z kopii zapasowej.",
|
||||
"keys.server.decrypt.failed": "Nie udało się odszyfrować kluczy konta przy użyciu tego hasła.",
|
||||
|
|
@ -201,5 +202,60 @@
|
|||
"message.unpinned.by": "{0} odpiął {1}.",
|
||||
"title.pinned.messages": "Przypięte",
|
||||
"error.pinned.messages.empty": "Brak przypiętych wiadomości",
|
||||
"title.message.lowercase": "wiadomość"
|
||||
"title.message.lowercase": "wiadomość",
|
||||
"action.rotate.keys": "Rotuj Klucze",
|
||||
"already.in.group": "Już w grupie",
|
||||
"desc.key.rotation.info": "Musisz zrotować klucze szyfrujące, aby kontynuować wysyłanie wiadomości w tej grupie. Inni członkowie automatycznie otrzymają nowy klucz.",
|
||||
"desc.no.groups": "Nie jesteś jeszcze w żadnej grupie.",
|
||||
"dm.read": "Przeczytano",
|
||||
"empty.body": "Pusta zawartość",
|
||||
"failed.create.group": "Nie udało się utworzyć grupy",
|
||||
"federation.not.implemented": "Federacja niezaimplementowana",
|
||||
"group.not.found": "Nie znaleziono grupy",
|
||||
"group.read": "Przeczytano",
|
||||
"invalid.entry.name": "Nieprawidłowa nazwa wpisu",
|
||||
"invalid.key.index": "Nieprawidłowy indeks klucza (odśwież stronę)",
|
||||
"invalid.msg.id": "Nieprawidłowe ID wiadomości",
|
||||
"invalid.payload": "Nieprawidłowy ładunek",
|
||||
"invalid.reaction.id": "Nieprawidłowe ID reakcji",
|
||||
"invalid.user": "Nieprawidłowy użytkownik",
|
||||
"joined": "Dołączono",
|
||||
"key.rotation.required": "Wymagana rotacja kluczy",
|
||||
"key.too.large": "Klucz zbyt duży",
|
||||
"keys.public.mismatch": "Niezgodność kluczy publicznych",
|
||||
"label.members": "CZŁONKOWIE",
|
||||
"message.deleted": "Wiadomość usunięta",
|
||||
"message.not.found": "Nie znaleziono wiadomości",
|
||||
"message.pin.toggled": "Zmieniono przypięcie wiadomości",
|
||||
"message.reacted": "Dodano reakcję",
|
||||
"message.reaction.removed": "Reakcja usunięta",
|
||||
"no.invite": "Brak zaproszenia",
|
||||
"not.a.member": "Nie jesteś członkiem",
|
||||
"not.in.group": "Nie jesteś w grupie",
|
||||
"reaction.not.found": "Nie znaleziono reakcji",
|
||||
"rotation.not.required": "Rotacja niewymagana",
|
||||
"storage.entry.updated": "Zaktualizowano wpis pamięci",
|
||||
"storage.limit.exceeded": "Przekroczono limit pamięci",
|
||||
"title.key.rotation": "Rotacja kluczy",
|
||||
"missing.keys": "Brakujące klucze",
|
||||
"error:messages.decrypt.failed": "Deszyfrowanie nie powiodło się",
|
||||
"error.message.already.deleted": "Wiadomość została już usunięta",
|
||||
"group.user.joined.by": "{0} dołączył do grupy",
|
||||
"group.created.by": "Grupa utworzona przez {0}",
|
||||
"group.user.left.by": "{0} opuścił grupę",
|
||||
"action.group.fetch": "Pobieranie grup",
|
||||
"group.created": "Utworzono grupę",
|
||||
"group.user.joined": "Dołączono do grupy",
|
||||
"prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:",
|
||||
"desc.failed.load.members": "Nie udało się załadować członków",
|
||||
"prompt.are.you.sure": "Czy na pewno?",
|
||||
"title.unknown.group": "Nieznana grupa",
|
||||
"title.unknown": "Nieznany",
|
||||
"key.rotated": "Klucze grupy zostały pomyślnie zrotowane.",
|
||||
"success:banner.updated": "Baner został pomyślnie zaktualizowany",
|
||||
"success:profile.updated": "Profil został pomyślnie zaktualizowany",
|
||||
"success:avatar.updated": "Awatar został pomyślnie zaktualizowany",
|
||||
"en-us": "Angielski",
|
||||
"pl-pl": "Polski",
|
||||
"en-cat": "Koci (Meowlish)"
|
||||
}
|
||||
|
|
@ -119,6 +119,25 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rotation-popup-container" class="profile-popup-container" style="display: none; z-index: 9999;">
|
||||
<div class="profile-popup-bg" onclick="closeRotationPopup()"></div>
|
||||
<div class="profile-popup-content">
|
||||
<button class="profile-popup-close-btn" onclick="closeRotationPopup()">
|
||||
<script>
|
||||
document.currentScript.insertAdjacentHTML('beforebegin', ICONS.close());
|
||||
document.currentScript.remove();
|
||||
</script>
|
||||
</button>
|
||||
<div id="profile-popup-inner" style="padding: 2rem; display: flex; flex-direction: column; align-items: center; gap: 1rem;">
|
||||
<div style="font-size: 1.2rem; font-weight: 500; text-align: center;"><blah>title.key.rotation</blah></div>
|
||||
<div style="width: 100%; text-align: center; color: var(--text-color); opacity: 0.8; margin-top: 1rem; margin-bottom: 1rem;" id="rotation-popup-desc">
|
||||
<!-- content injected dynamically -->
|
||||
</div>
|
||||
<button class="profile-action-btn" onclick="executeKeyRotation()"><blah>action.rotate.keys</blah></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -208,8 +227,59 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function createGroup() {
|
||||
async function refreshGroups(waittime = 0, showLoading = true) {
|
||||
try {
|
||||
if (showLoading) showAction("action.group.fetch", "grouprefresh");
|
||||
await delay(waittime);
|
||||
if (window.cachedGroups && typeof renderGroups === 'function') {
|
||||
await renderGroups(window.cachedGroups);
|
||||
}
|
||||
let res = await fetchEncrypted("user/group/list");
|
||||
window.cachedGroups = res;
|
||||
if (typeof renderGroups === 'function') {
|
||||
await renderGroups(res);
|
||||
}
|
||||
if (showLoading) clearAction("grouprefresh");
|
||||
}
|
||||
catch (e) {
|
||||
if (showLoading) clearAction("grouprefresh");
|
||||
showBlahNotification("error:group.refresh.failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function createGroup() {
|
||||
try {
|
||||
let groupName = document.getElementById("creategroup-name").value;
|
||||
if (!groupName || groupName.trim() === "") {
|
||||
showBlahNotification("error:group.name.empty");
|
||||
return;
|
||||
}
|
||||
|
||||
showAction("action.group.creating", "group.action");
|
||||
|
||||
await ensureUserKeys();
|
||||
const roomKey = randomRoomKey256Chars();
|
||||
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupName,
|
||||
string2: selfWrapped
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/create", payload);
|
||||
clearAction("group.action");
|
||||
|
||||
if (res.startsWith("success:")) {
|
||||
showBlahNotification("success:group.created");
|
||||
await refreshGroups(200, false);
|
||||
} else {
|
||||
showBlahNotification(res || "error:something.wrong");
|
||||
}
|
||||
} catch (e) {
|
||||
clearAction("group.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="gifuct.js"></script>
|
||||
|
|
|
|||
|
|
@ -367,6 +367,171 @@ async function ensureDmRoomKey(dmId) {
|
|||
|
||||
window.ensureDmRoomKey = ensureDmRoomKey;
|
||||
|
||||
window.groupKeysCache = {};
|
||||
|
||||
async function getGroupKey(groupId, keyIndex) {
|
||||
if (!window.groupKeysCache[groupId]) {
|
||||
window.groupKeysCache[groupId] = {};
|
||||
}
|
||||
if (window.groupKeysCache[groupId][keyIndex]) {
|
||||
return window.groupKeysCache[groupId][keyIndex];
|
||||
}
|
||||
|
||||
let payload = JSON.stringify({string1: groupId, string2: keyIndex.toString()});
|
||||
let res = await fetchEncrypted("group/key/get", payload);
|
||||
if (!res || res.startsWith("error:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let decryptedKey;
|
||||
if (res.startsWith("SETUP:")) {
|
||||
const rest = res.substring("SETUP:".length);
|
||||
const parts = rest.split(";");
|
||||
if (parts.length < 2) throw new Error("error:group.key.setup.malformed");
|
||||
|
||||
const otherPublicJson = parts[0];
|
||||
const setupPayloadJson = parts.slice(1).join(";");
|
||||
const otherPublic = JSON.parse(otherPublicJson);
|
||||
const setupPayload = JSON.parse(setupPayloadJson);
|
||||
|
||||
const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem);
|
||||
decryptedKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
|
||||
} else {
|
||||
decryptedKey = await unwrapRoomKeyForSelf(res);
|
||||
}
|
||||
|
||||
window.groupKeysCache[groupId][keyIndex] = decryptedKey;
|
||||
return decryptedKey;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeRotationPopup() {
|
||||
let popupContainer = document.getElementById("rotation-popup-container");
|
||||
if(popupContainer) {
|
||||
popupContainer.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
popupContainer.style.display = "none";
|
||||
}, 200);
|
||||
}
|
||||
clearAction("key_rotation");
|
||||
}
|
||||
|
||||
let pendingRotationMessagePayload = null;
|
||||
|
||||
async function showRotationPopup(groupId, originalMessagePayload) {
|
||||
pendingRotationMessagePayload = originalMessagePayload;
|
||||
showAction("action.key.rotation", "key_rotation");
|
||||
|
||||
try {
|
||||
let membersRaw = await fetchEncrypted("group/members", groupId);
|
||||
if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw);
|
||||
|
||||
let members = JSON.parse(membersRaw);
|
||||
|
||||
let desc = document.getElementById("rotation-popup-desc");
|
||||
desc.innerHTML = `<blah>desc.key.rotation.info</blah><br><br><b>${members.length} <blah>label.members</blah></b>`;
|
||||
let blahTags = desc.getElementsByTagName("blah");
|
||||
for (let i = 0; i < blahTags.length; i++) {
|
||||
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
|
||||
}
|
||||
|
||||
let popupContainer = document.getElementById("rotation-popup-container");
|
||||
popupContainer.style.display = "flex";
|
||||
popupContainer.offsetHeight; // trigger reflow
|
||||
popupContainer.classList.add("show");
|
||||
} catch(e) {
|
||||
clearAction("key_rotation");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function executeKeyRotation() {
|
||||
closeRotationPopup();
|
||||
showAction("action.key.rotating", "key_rotation_exec");
|
||||
|
||||
try {
|
||||
let groupId = currentDmId;
|
||||
|
||||
let membersRaw = await fetchEncrypted("group/members", groupId);
|
||||
if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw);
|
||||
let members = JSON.parse(membersRaw);
|
||||
|
||||
let roomKey = randomRoomKey256Chars();
|
||||
let keysDict = {};
|
||||
let myPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${window.id}`);
|
||||
|
||||
for(let memberId of members) {
|
||||
let pureId = memberId.split(";")[0];
|
||||
if(pureId === window.id) {
|
||||
keysDict[memberId] = await wrapRoomKeyForSelf(roomKey);
|
||||
} else {
|
||||
let publicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${pureId}`);
|
||||
if(publicKeysRaw && publicKeysRaw.length > 10) {
|
||||
let publicKeys = JSON.parse(publicKeysRaw);
|
||||
let hybrid = await deriveHybridKeyToUser(publicKeys);
|
||||
let encForMember = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes);
|
||||
let setupPayloadJson = JSON.stringify({
|
||||
v: 1,
|
||||
alg: "hybrid-x25519-mlkem768",
|
||||
ctMlKem: hybrid.ciphertextMlKemBase64,
|
||||
enc: encForMember
|
||||
});
|
||||
keysDict[memberId] = `SETUP:${myPublicKeysRaw};${setupPayloadJson}`;
|
||||
} else {
|
||||
throw new Error("error:missing.keys");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let payload = JSON.stringify({
|
||||
groupId: groupId,
|
||||
keys: keysDict
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/key/rotate", payload);
|
||||
if(res && res.startsWith("error:")) {
|
||||
throw new Error(res);
|
||||
}
|
||||
|
||||
// Success!
|
||||
let newIndex = res.replace("success:", "");
|
||||
window.currentGroupKeyIndex = newIndex;
|
||||
|
||||
if(!window.groupKeysCache[groupId]) window.groupKeysCache[groupId] = {};
|
||||
window.groupKeysCache[groupId][newIndex] = roomKey;
|
||||
|
||||
clearAction("key_rotation_exec");
|
||||
showBlahNotification("success:key.rotated");
|
||||
|
||||
// Retry sending the message!
|
||||
if(pendingRotationMessagePayload) {
|
||||
let p = pendingRotationMessagePayload;
|
||||
p.string5 = newIndex;
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(roomKey));
|
||||
p.string2 = await encryptAesGcmToBase64(p.rawPlainContent, dmKeyBytes); // we need to save rawPlainContent for retry
|
||||
|
||||
showAction("action.message.sending", "msgsending");
|
||||
let resSend = await fetchEncrypted("group/message/send", JSON.stringify(p));
|
||||
clearAction("msgsending");
|
||||
|
||||
if(resSend && resSend.startsWith("error:")) {
|
||||
showBlahNotification(resSend);
|
||||
} else if(resSend && resSend.startsWith("success:")) {
|
||||
loadDmMessages(currentDmId);
|
||||
}
|
||||
}
|
||||
pendingRotationMessagePayload = null;
|
||||
|
||||
} catch(e) {
|
||||
clearAction("key_rotation_exec");
|
||||
console.error(e);
|
||||
showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function encrypt(plainText, keyBytes) {
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const encoder = new TextEncoder();
|
||||
|
|
@ -857,6 +1022,18 @@ async function getAvatarUrl(id, username) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getGroupAvatarUrl(groupId, groupName) {
|
||||
try {
|
||||
let pfpUrl = `${url}/group/storage/public/getentry?id=${groupId}&e=larp.profile.pfp`;
|
||||
if ((await fetchAsync(pfpUrl)) == "") {
|
||||
throw Error();
|
||||
}
|
||||
return pfpUrl;
|
||||
} catch (e) {
|
||||
return createAvatarSvg(groupName);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoadingStatus(message) {
|
||||
loadingStatus.innerHTML = processBlah(message);
|
||||
}
|
||||
|
|
@ -1743,6 +1920,71 @@ function setActiveRoombarItem(itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
async function acceptGroupInvite(groupId, inviterId) {
|
||||
try {
|
||||
showAction("action.invite.accepting", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: inviterId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/join", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:joined");
|
||||
|
||||
// Refresh invites and groups
|
||||
window.cachedInvites = {'received': null, 'sent': null};
|
||||
loadInvites("received");
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function declineGroupInvite(groupId, inviterId) {
|
||||
try {
|
||||
showAction("action.invite.declining", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: inviterId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/invite/decline", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:invite.declined");
|
||||
|
||||
window.cachedInvites = {'received': null, 'sent': null};
|
||||
loadInvites("received");
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeGroupInvite(groupId, targetId) {
|
||||
try {
|
||||
showAction("action.invite.revoking", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: targetId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/invite/revoke", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:invite.revoked");
|
||||
|
||||
window.cachedInvites = {'received': null, 'sent': null};
|
||||
loadInvites("sent");
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function renderInvites(res, type) {
|
||||
let container = document.getElementById("invites-container");
|
||||
if (!container) return;
|
||||
|
|
@ -1781,7 +2023,7 @@ async function renderInvites(res, type) {
|
|||
}
|
||||
let onlyId = id.split(":")[0];
|
||||
let username = await fetchAsync(`${url}/idtoname?id=${id}`);
|
||||
if (!(username && username.trim() !== "")) return;
|
||||
if (!(username && username.trim() !== "")) continue;
|
||||
count++;
|
||||
|
||||
let pfp = await getAvatarUrl(id, username);
|
||||
|
|
@ -1789,8 +2031,9 @@ async function renderInvites(res, type) {
|
|||
let actions = "";
|
||||
let desc = "";
|
||||
|
||||
let displayName = username.split(':')[0] || onlyId;
|
||||
if (type === "received") {
|
||||
desc = `:desc.invite.dm.received:${username};${onlyId}`;
|
||||
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-green);" onclick="acceptInvite('${id}')">
|
||||
${ICONS.check({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
|
|
@ -1800,7 +2043,7 @@ async function renderInvites(res, type) {
|
|||
</button>
|
||||
`;
|
||||
} else {
|
||||
desc = `:desc.invite.dm.sent:${username};${onlyId}`;
|
||||
desc = `:desc.invite.dm.sent:${displayName};${onlyId}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-red);" onclick="revokeInvite('${id}')">
|
||||
${ICONS.close({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
|
|
@ -1809,12 +2052,70 @@ async function renderInvites(res, type) {
|
|||
}
|
||||
|
||||
let entry = invitesEntry
|
||||
.replaceAll("{username}", username.split(':')[0])
|
||||
.replaceAll("{username}", displayName)
|
||||
.replaceAll("{pfp}", pfp)
|
||||
.replaceAll("{desc}", desc)
|
||||
.replaceAll("{desc}", `<blah>${desc}</blah>`)
|
||||
.replaceAll("{actions}", actions)
|
||||
.replaceAll("{onclick_attr}", `onclick="openProfile('${id}')" style="cursor: pointer;"`)
|
||||
.replaceAll("{id}", id);
|
||||
|
||||
html += entry;
|
||||
} else if (invite.type === "group") {
|
||||
let groupId = invite.id;
|
||||
let targetId = (type === "received") ? invite.sender : invite.receiver;
|
||||
|
||||
if (!targetId.includes(":")) {
|
||||
targetId += `:${host}`;
|
||||
}
|
||||
let onlyId = targetId.split(":")[0];
|
||||
let username = await fetchAsync(`${url}/idtoname?id=${targetId}`);
|
||||
if (!(username && username.trim() !== "")) continue;
|
||||
|
||||
// Fetch group name
|
||||
let groupInfoStr = await fetchEncrypted("group/info", groupId);
|
||||
let groupName = processBlah("title.unknown.group");
|
||||
if (groupInfoStr && !groupInfoStr.startsWith("error")) {
|
||||
let groupInfo = JSON.parse(groupInfoStr);
|
||||
groupName = groupInfo.string2;
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
let pfp = await getGroupAvatarUrl(groupId, groupName);
|
||||
let actions = "";
|
||||
let desc = "";
|
||||
|
||||
let displayName = username.split(':')[0] || onlyId;
|
||||
let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`;
|
||||
let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`;
|
||||
|
||||
if (type === "received") {
|
||||
desc = `:desc.invite.group.received:${val1};${val2}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-green);" onclick="acceptGroupInvite('${groupId}', '${targetId}')">
|
||||
${ICONS.check({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
</button>
|
||||
<button class="icon-button" style="background-color: var(--big-red);" onclick="declineGroupInvite('${groupId}', '${targetId}')">
|
||||
${ICONS.close({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
desc = `:desc.invite.group.sent:${val1};${val2}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-red);" onclick="revokeGroupInvite('${groupId}', '${targetId}')">
|
||||
${ICONS.close({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
let entry = invitesEntry
|
||||
.replaceAll("{username}", groupName)
|
||||
.replaceAll("{pfp}", pfp)
|
||||
.replaceAll("{desc}", `<blah>${desc}</blah>`)
|
||||
.replaceAll("{actions}", actions)
|
||||
.replaceAll("{onclick_attr}", "")
|
||||
.replaceAll("{id}", groupId);
|
||||
|
||||
html += entry;
|
||||
}
|
||||
}
|
||||
|
|
@ -1996,7 +2297,10 @@ async function gotoHome(splash = true) {
|
|||
switchRoomsBar("title.home", homeRoomBar);
|
||||
setActiveRoombarItem(null);
|
||||
setupWebSocket();
|
||||
await refreshDms(210);
|
||||
await Promise.all([
|
||||
refreshDms(210),
|
||||
(typeof refreshGroups === 'function' ? refreshGroups(210) : Promise.resolve())
|
||||
]);
|
||||
}
|
||||
|
||||
function setActiveSidebarIndicator(element, isTemporary = false) {
|
||||
|
|
@ -2210,6 +2514,67 @@ async function renderDms(res) {
|
|||
}
|
||||
}
|
||||
|
||||
async function renderGroups(res) {
|
||||
let container = document.getElementById("groups-list");
|
||||
if (!container) return;
|
||||
|
||||
if (res && res !== "") {
|
||||
let parsed = JSON.parse(res);
|
||||
let groups = parsed.groups || {};
|
||||
let groupEntries = Object.entries(groups);
|
||||
|
||||
//Sort by timestamp descending
|
||||
groupEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
|
||||
|
||||
let html = "";
|
||||
let count = 0;
|
||||
|
||||
for (let [groupId, groupData] of groupEntries) {
|
||||
let isRead = groupData.string3 === "true";
|
||||
let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";
|
||||
|
||||
let infoRes = await fetchEncrypted("group/info", groupId);
|
||||
if (infoRes && infoRes.startsWith("{")) {
|
||||
let info = JSON.parse(infoRes);
|
||||
let groupName = info.string2;
|
||||
|
||||
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
|
||||
let pfp = await getGroupAvatarUrl(groupId, groupName);
|
||||
|
||||
html += `
|
||||
<button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')">
|
||||
<img src="${pfp}" class="room-pfp">
|
||||
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||
<span class="room-name" style="${nameStyle}">${groupName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
container.innerHTML = html;
|
||||
container.classList.remove("empty");
|
||||
} else {
|
||||
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.groups</blah></p>`;
|
||||
container.classList.add("empty");
|
||||
let blahTags = container.getElementsByTagName("blah");
|
||||
for (let i = 0; i < blahTags.length; i++) {
|
||||
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.groups</blah></p>`;
|
||||
container.classList.add("empty");
|
||||
let blahTags = container.getElementsByTagName("blah");
|
||||
for (let i = 0; i < blahTags.length; i++) {
|
||||
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentDmId = null;
|
||||
var currentDmKey = null; // AES key for encrypting/decrypting messages
|
||||
var dmUsernameCache = {};
|
||||
|
|
@ -2263,30 +2628,131 @@ async function generateDmMembersHtml(targetId, targetUsername) {
|
|||
`;
|
||||
}
|
||||
|
||||
async function openDm(dmId, username, targetId) {
|
||||
async function inviteToGroupPrompt(groupId) {
|
||||
let targetUsername = prompt(processBlah("prompt.invite.username"));
|
||||
if (!targetUsername) return;
|
||||
|
||||
if (!targetUsername.includes(":")) {
|
||||
targetUsername += `:${host}`;
|
||||
}
|
||||
|
||||
let idRes = await fetchAsync(`${url}/nametoid?u=${targetUsername}`);
|
||||
if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") {
|
||||
showBlahNotification("error:user.not.found");
|
||||
return;
|
||||
}
|
||||
let targetId = idRes.trim();
|
||||
|
||||
try {
|
||||
showAction("action.invite.sending", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: targetId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/invite", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:invite.sent");
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGroupMembersHtml(groupId, groupName) {
|
||||
let membersJson = await fetchEncrypted("group/members", groupId);
|
||||
if (!membersJson || membersJson.startsWith("error")) {
|
||||
return `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.failed.load.members</blah></div>`;
|
||||
}
|
||||
|
||||
let members = JSON.parse(membersJson);
|
||||
let html = `
|
||||
<div class="list-container no-flex-grow" style="padding: 0 0.5rem; width: 100%;">
|
||||
<div class="sidebar-section-header" style="margin-top: 0.5rem; margin-bottom: 0.5rem; width: 100%; display: flex; align-items: center;">
|
||||
<span style="padding-left: 0.6rem; font-size: 0.8rem; font-weight: bold; opacity: 0.5; text-transform: uppercase;"><blah>title.members</blah></span>
|
||||
<div class="flex-spacer"></div>
|
||||
<button class="topbar-action-btn" onclick="inviteToGroupPrompt('${groupId}')" style="margin-right: 0.5rem; width: 1.2rem; height: 1.2rem; display: flex; align-items: center; justify-content: center; opacity: 0.7;">
|
||||
${ICONS.plus({ width: "1rem", height: "1rem" })}
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
for (let memberId of members) {
|
||||
let memberUsername = await fetchAsync(`${url}/idtoname?id=${memberId}`);
|
||||
if (!memberUsername || memberUsername.startsWith("error")) continue;
|
||||
|
||||
let memberPfp = await getAvatarUrl(memberId, memberUsername);
|
||||
let memberDisplayName = memberUsername.split(':')[0];
|
||||
|
||||
html += `
|
||||
<button class="room-entry" onclick="openProfile('${memberId}')" style="margin-bottom: 0;">
|
||||
<img src="${memberPfp}" class="room-pfp">
|
||||
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||
<span class="room-name">${memberDisplayName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${memberUsername}</span>
|
||||
</div>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
async function openDm(dmId, username, targetId, type = 'dms') {
|
||||
try {
|
||||
showAction("action.dm.opening", "dmopen");
|
||||
|
||||
markDmAsRead(dmId);
|
||||
if (type === 'groups') {
|
||||
markDmAsRead(dmId, 'groups');
|
||||
} else {
|
||||
markDmAsRead(dmId, 'dms');
|
||||
}
|
||||
|
||||
if (type === 'dms') {
|
||||
currentDmKey = await ensureDmRoomKey(dmId);
|
||||
if (!currentDmKey) {
|
||||
throw new Error("Missing DM room key");
|
||||
}
|
||||
} else {
|
||||
let infoPayload = await fetchEncrypted("group/info", dmId);
|
||||
if (infoPayload && !infoPayload.startsWith("error:")) {
|
||||
let info = JSON.parse(infoPayload);
|
||||
window.currentGroupKeyIndex = info.string4 || "0";
|
||||
} else {
|
||||
window.currentGroupKeyIndex = "0";
|
||||
}
|
||||
}
|
||||
|
||||
if (currentDmId !== dmId) {
|
||||
currentRoomContentTitle = "";
|
||||
}
|
||||
currentDmId = dmId;
|
||||
window.currentRoomType = type; // Save the type globally
|
||||
loadedMessages = {};
|
||||
oldestLoadedMsgId = null;
|
||||
isLoadingOlderMessages = false;
|
||||
|
||||
let iconHtml = "";
|
||||
if (type === 'dms') {
|
||||
let pfp = await getAvatarUrl(targetId, username);
|
||||
let iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||
iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||
} else {
|
||||
let pfp = await getGroupAvatarUrl(dmId, username);
|
||||
iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||
}
|
||||
|
||||
if (currentRoomsBarTitle !== "title.home") {
|
||||
gotoHome(false);
|
||||
}
|
||||
|
||||
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
||||
let membersHtml = await generateDmMembersHtml(targetId, username);
|
||||
|
||||
let membersHtml = "";
|
||||
if (type === 'dms') {
|
||||
membersHtml = await generateDmMembersHtml(targetId, username);
|
||||
} else {
|
||||
membersHtml = await generateGroupMembersHtml(dmId, username);
|
||||
}
|
||||
await switchDetailsContent("title.details", membersHtml);
|
||||
|
||||
let msgContainer = document.getElementById("chat-messages");
|
||||
|
|
@ -2326,7 +2792,8 @@ async function openDm(dmId, username, targetId) {
|
|||
async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
||||
try {
|
||||
let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset});
|
||||
let res = await fetchEncrypted("dm/messages/get", payload);
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/messages/get" : "dm/messages/get";
|
||||
let res = await fetchEncrypted(endpoint, payload);
|
||||
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
|
|
@ -2389,9 +2856,26 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let decrypted = false;
|
||||
if (msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
keyToUse = await getGroupKey(currentDmId, msg.key);
|
||||
if (keyToUse) {
|
||||
let mK = parseInt(msg.key);
|
||||
let cK = parseInt(window.currentGroupKeyIndex || "0");
|
||||
if (!isNaN(mK) && !isNaN(cK) && mK > cK) {
|
||||
window.currentGroupKeyIndex = msg.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyToUse) {
|
||||
content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
|
||||
decrypted = false;
|
||||
} else {
|
||||
let keyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
content = await decryptAesGcmFromBase64(content, keyBytes);
|
||||
decrypted = true;
|
||||
}
|
||||
} catch (e) {
|
||||
content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
|
||||
decrypted = false;
|
||||
|
|
@ -2423,7 +2907,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let safeType = encodeURIComponent(att.type).replace(/'/g, "%27");
|
||||
let safeName = encodeURIComponent(att.name).replace(/'/g, "%27");
|
||||
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'))">
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'), '${msg.key || "0"}')">
|
||||
<div style="padding: 1rem; text-align: center; color: var(--text-color);">
|
||||
${iconSvg}
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; word-break: break-all;">${escapeHtml(displayName)}</div>
|
||||
|
|
@ -2454,8 +2938,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
content = processBlah(content);
|
||||
|
||||
if (msg.type && msg.type.startsWith("larp.info")) {
|
||||
if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin") {
|
||||
let authorName = "Unknown";
|
||||
if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin" || msg.type === "larp.info.join" || msg.type === "larp.info.leave" || msg.type === "larp.info.create") {
|
||||
let authorName = processBlah("title.unknown");
|
||||
if (msg.author && msg.author !== "0") {
|
||||
if (!dmUsernameCache[msg.author]) {
|
||||
let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`);
|
||||
|
|
@ -2471,14 +2955,22 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
}
|
||||
|
||||
let isPinned = msg.type === "larp.info.pin";
|
||||
let actionStr = isPinned ? "message.pinned.by" : "message.unpinned.by";
|
||||
let actionStr = "";
|
||||
if (msg.type === "larp.info.pin") actionStr = "message.pinned.by";
|
||||
else if (msg.type === "larp.info.unpin") actionStr = "message.unpinned.by";
|
||||
else if (msg.type === "larp.info.join") actionStr = "group.user.joined.by";
|
||||
else if (msg.type === "larp.info.leave") actionStr = "group.user.left.by";
|
||||
else if (msg.type === "larp.info.create") actionStr = "group.created.by";
|
||||
|
||||
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
|
||||
|
||||
let textStr = processBlah(actionStr);
|
||||
if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) {
|
||||
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg);
|
||||
} else {
|
||||
content = textStr.replace("{0}", clickableAuthor);
|
||||
}
|
||||
}
|
||||
html += `<div style="text-align: center; opacity: 0.5; padding: 1rem; font-size: 0.9rem; word-break: break-word; overflow-wrap: anywhere;">${content}</div>`;
|
||||
lastAuthor = null;
|
||||
|
|
@ -2501,7 +2993,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
lastAuthor = msg.author;
|
||||
|
||||
let authorName = "Unknown";
|
||||
let authorName = processBlah("title.unknown");
|
||||
let pfp = "assets/default_avatar.png";
|
||||
if (msg.author !== "0") {
|
||||
if (!dmUsernameCache[msg.author]) {
|
||||
|
|
@ -2618,8 +3110,15 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let rxVal = rx.r;
|
||||
if (msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, msg.key);
|
||||
if (k) keyToUse = k;
|
||||
}
|
||||
if (keyToUse) {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false, myReactionId: null};
|
||||
|
|
@ -2839,8 +3338,8 @@ function setupChatScrollListener() {
|
|||
|
||||
|
||||
|
||||
async function downloadAndRenderAttachment(container, msgId, attId, type, name) {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
async function downloadAndRenderAttachment(container, msgId, attId, type, name, keyIndex = "0") {
|
||||
if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return;
|
||||
if (!container) return;
|
||||
|
||||
if (container.dataset.loaded === "true") {
|
||||
|
|
@ -2860,10 +3359,16 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name)
|
|||
container.innerHTML = `<div style="padding: 1rem; color: var(--text-color);">${ICONS.spinner()}</div>`;
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, keyIndex);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
|
||||
let res = await fetch(`${url}/dm/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, {
|
||||
let endpointType = window.currentRoomType === 'groups' ? "group" : "dm";
|
||||
let res = await fetch(`${url}/${endpointType}/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, {
|
||||
headers: {
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
}
|
||||
|
|
@ -2953,7 +3458,7 @@ function renderAttachmentsPreview() {
|
|||
}
|
||||
|
||||
async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return;
|
||||
|
||||
let input = document.getElementById("chat-input");
|
||||
let content = input.value.trim();
|
||||
|
|
@ -2975,7 +3480,13 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
}
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, window.currentGroupKeyIndex || "0");
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||
|
||||
let attachmentsPayload = [];
|
||||
|
|
@ -3036,6 +3547,11 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
string4: replyingToMsgId || ""
|
||||
};
|
||||
|
||||
if (window.currentRoomType === 'groups') {
|
||||
msgPayload.string5 = window.currentGroupKeyIndex || "0";
|
||||
msgPayload.rawPlainContent = content; // for retry
|
||||
}
|
||||
|
||||
input.value = "";
|
||||
input.style.height = '2.5rem';
|
||||
sendbutton.style.height = '2.5rem';
|
||||
|
|
@ -3049,9 +3565,14 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
|
||||
showAction("action.message.sending", "msgsending");
|
||||
|
||||
let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
|
||||
let res = await fetchEncrypted(window.currentRoomType === 'groups' ? "group/message/send" : "dm/message/send", JSON.stringify(msgPayload));
|
||||
clearAction("msgsending");
|
||||
if (res && res.startsWith("error:")) {
|
||||
|
||||
if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') {
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
showRotationPopup(currentDmId, msgPayload);
|
||||
} else if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
|
|
@ -3073,7 +3594,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
type: "larp.text",
|
||||
content: encryptedContent,
|
||||
attachment: finalAttPayload,
|
||||
key: "0",
|
||||
key: window.currentRoomType === 'groups' ? (window.currentGroupKeyIndex || "0") : "0",
|
||||
pervious: "",
|
||||
responded: replyingToMsgId || "",
|
||||
reactions: ""
|
||||
|
|
@ -3148,15 +3669,16 @@ function setupWebSocket() {
|
|||
|
||||
appWebSocket.onmessage = (event) => {
|
||||
let data = event.data;
|
||||
if (data.startsWith("dm_message:")) {
|
||||
if (data.startsWith("dm_message:") || data.startsWith("group_message:")) {
|
||||
let parts = data.split(":");
|
||||
let msgTypeRoom = data.startsWith("dm_message:") ? "dms" : "groups";
|
||||
let msgDmId = parts[1];
|
||||
let msgType = parts[2] || "unread";
|
||||
|
||||
if (msgType === "unread") {
|
||||
updateDmListUI(msgDmId, false, Date.now().toString());
|
||||
updateDmListUI(msgDmId, false, msgTypeRoom, Date.now().toString());
|
||||
} else if (msgType === "read") {
|
||||
updateDmListUI(msgDmId, true, Date.now().toString());
|
||||
updateDmListUI(msgDmId, true, msgTypeRoom, Date.now().toString());
|
||||
} else if (msgType === "update") {
|
||||
// do nothing to UI sorting/read state
|
||||
}
|
||||
|
|
@ -3246,7 +3768,8 @@ async function togglePinMessage(msgId) {
|
|||
string1: currentDmId,
|
||||
string2: msgId
|
||||
});
|
||||
await fetchEncrypted("dm/message/pin/toggle", payload);
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/message/pin/toggle" : "dm/message/pin/toggle";
|
||||
await fetchEncrypted(endpoint, payload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showBlahNotification("error.network.error");
|
||||
|
|
@ -3270,7 +3793,8 @@ async function deleteMessage(msgId) {
|
|||
string1: currentDmId,
|
||||
string2: msgId
|
||||
};
|
||||
let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload));
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/message/delete" : "dm/message/delete";
|
||||
let res = await fetchEncrypted(endpoint, JSON.stringify(msgPayload));
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
}
|
||||
|
|
@ -3346,8 +3870,15 @@ async function replyMessage(msgId) {
|
|||
let content = msg ? msg.content : "";
|
||||
if (msg && msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, msg.key);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
if (keyToUse) {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
|
@ -3454,8 +3985,15 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
let rxVal = rx.r;
|
||||
if (currentMsg.key && currentMsg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, currentMsg.key);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
if (keyToUse) {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (rx.u === id && rxVal === reaction) {
|
||||
|
|
@ -3468,10 +4006,17 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
}
|
||||
|
||||
let isAdding = (myReactionId === null);
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, currentMsg.key);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
let encryptedReaction = await encryptAesGcmToBase64(reaction, dmKeyBytes);
|
||||
|
||||
let endpoint = isAdding ? "dm/message/react/add" : "dm/message/react/remove";
|
||||
let endpoint = window.currentRoomType === 'groups'
|
||||
? (isAdding ? "group/message/react/add" : "group/message/react/remove")
|
||||
: (isAdding ? "dm/message/react/add" : "dm/message/react/remove");
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId,
|
||||
|
|
@ -3615,7 +4160,7 @@ async function changePassword() {
|
|||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
if (!confirm(processBlah("desc.delete.account") + "\n\nAre you sure?")) return;
|
||||
if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return;
|
||||
showAction("action.account.deleting", "accountdel");
|
||||
let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, "");
|
||||
clearAction("accountdel");
|
||||
|
|
@ -3978,19 +4523,52 @@ if (container) {
|
|||
document.addEventListener('touchend', endDrag);
|
||||
}
|
||||
|
||||
function updateDmListUI(dmId, isRead, timestamp = null) {
|
||||
function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
|
||||
try {
|
||||
if (!window.cachedDms) return;
|
||||
let parsedDms = JSON.parse(window.cachedDms);
|
||||
if (!parsedDms.dms || !parsedDms.dms[dmId]) return;
|
||||
if (type === 'groups') {
|
||||
if (!window.cachedGroups) return;
|
||||
let parsedGroups = JSON.parse(window.cachedGroups);
|
||||
if (!parsedGroups.groups || !parsedGroups.groups[roomId]) return;
|
||||
|
||||
let changed = false;
|
||||
if (parsedDms.dms[dmId].string3 !== (isRead ? "true" : "false")) {
|
||||
parsedDms.dms[dmId].string3 = isRead ? "true" : "false";
|
||||
if (parsedGroups.groups[roomId].string3 !== (isRead ? "true" : "false")) {
|
||||
parsedGroups.groups[roomId].string3 = isRead ? "true" : "false";
|
||||
changed = true;
|
||||
}
|
||||
if (timestamp && parsedDms.dms[dmId].string2 !== timestamp) {
|
||||
parsedDms.dms[dmId].string2 = timestamp;
|
||||
if (timestamp && parsedGroups.groups[roomId].string2 !== timestamp) {
|
||||
parsedGroups.groups[roomId].string2 = timestamp;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
window.cachedGroups = JSON.stringify(parsedGroups);
|
||||
}
|
||||
|
||||
let btn = document.getElementById(`group-btn-${roomId}`);
|
||||
let container = document.getElementById("groups-list");
|
||||
if (btn && container) {
|
||||
let nameSpan = btn.querySelector('.room-name');
|
||||
if (nameSpan) {
|
||||
nameSpan.style.opacity = isRead ? "0.8" : "1";
|
||||
}
|
||||
if (timestamp) {
|
||||
container.prepend(btn);
|
||||
}
|
||||
} else {
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
}
|
||||
} else {
|
||||
if (!window.cachedDms) return;
|
||||
let parsedDms = JSON.parse(window.cachedDms);
|
||||
if (!parsedDms.dms || !parsedDms.dms[roomId]) return;
|
||||
|
||||
let changed = false;
|
||||
if (parsedDms.dms[roomId].string3 !== (isRead ? "true" : "false")) {
|
||||
parsedDms.dms[roomId].string3 = isRead ? "true" : "false";
|
||||
changed = true;
|
||||
}
|
||||
if (timestamp && parsedDms.dms[roomId].string2 !== timestamp) {
|
||||
parsedDms.dms[roomId].string2 = timestamp;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -3998,7 +4576,7 @@ function updateDmListUI(dmId, isRead, timestamp = null) {
|
|||
window.cachedDms = JSON.stringify(parsedDms);
|
||||
}
|
||||
|
||||
let btn = document.getElementById(`dm-btn-${dmId}`);
|
||||
let btn = document.getElementById(`dm-btn-${roomId}`);
|
||||
let container = document.getElementById("dms-list");
|
||||
if (btn && container) {
|
||||
let nameSpan = btn.querySelector('.room-name');
|
||||
|
|
@ -4011,15 +4589,21 @@ function updateDmListUI(dmId, isRead, timestamp = null) {
|
|||
} else {
|
||||
if (typeof refreshDms === 'function') refreshDms(0, false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (typeof refreshDms === 'function') refreshDms(0, false);
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
function markDmAsRead(dmId) {
|
||||
fetchEncrypted("dm/read", JSON.stringify({string1: dmId})).catch(()=>{});
|
||||
updateDmListUI(dmId, true);
|
||||
function markDmAsRead(roomId, type = 'dms') {
|
||||
if (type === 'groups') {
|
||||
fetchEncrypted("group/read", JSON.stringify({string1: roomId})).catch(()=>{});
|
||||
} else {
|
||||
fetchEncrypted("dm/read", JSON.stringify({string1: roomId})).catch(()=>{});
|
||||
}
|
||||
updateDmListUI(roomId, true, type);
|
||||
}
|
||||
|
||||
function handleDmInteraction() {
|
||||
|
|
@ -4029,10 +4613,19 @@ function handleDmInteraction() {
|
|||
|
||||
if (lastChatScroll <= 10) {
|
||||
try {
|
||||
if (window.currentRoomType === 'groups') {
|
||||
if (window.cachedGroups) {
|
||||
let parsedGroups = JSON.parse(window.cachedGroups);
|
||||
if (parsedGroups.groups && parsedGroups.groups[currentDmId] && parsedGroups.groups[currentDmId].string3 !== "true") {
|
||||
markDmAsRead(currentDmId, window.currentRoomType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (window.cachedDms) {
|
||||
let parsedDms = JSON.parse(window.cachedDms);
|
||||
if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") {
|
||||
markDmAsRead(currentDmId);
|
||||
markDmAsRead(currentDmId, window.currentRoomType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
@ -4056,7 +4649,8 @@ async function showPinnedMessages(forceReload = false, showPanel = true) {
|
|||
}
|
||||
|
||||
try {
|
||||
let res = await fetchEncrypted("dm/messages/pinned", currentDmId);
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/messages/pinned" : "dm/messages/pinned";
|
||||
let res = await fetchEncrypted(endpoint, currentDmId);
|
||||
if (res && !res.startsWith("error:")) {
|
||||
let msgs = JSON.parse(res);
|
||||
let entries = Object.entries(msgs).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
||||
|
|
|
|||
|
|
@ -117,10 +117,10 @@ var notifisScreen = `
|
|||
var invitesEntry = `
|
||||
<div class="invite-entry">
|
||||
<div class="invite-info">
|
||||
<img src="{pfp}" class="invite-pfp" onclick="openProfile('{id}')" style="cursor: pointer;">
|
||||
<img src="{pfp}" class="invite-pfp" {onclick_attr}>
|
||||
<div class="invite-details">
|
||||
<span class="invite-name" onclick="openProfile('{id}')" style="cursor: pointer;">{username}</span>
|
||||
<span class="invite-desc"><blah>{desc}</blah></span>
|
||||
<span class="invite-name" {onclick_attr}>{username}</span>
|
||||
<span class="invite-desc">{desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invite-actions">
|
||||
|
|
|
|||
|
|
@ -215,6 +215,25 @@ sidebar {
|
|||
width: calc(var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width));
|
||||
border-right: var(--border-width) solid var(--light-border-color);
|
||||
}
|
||||
|
||||
sidebar:not(.second) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
sidebar:not(.second)::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
sidebar:not(.second) {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#roomsbar roomcontent2 {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
sidebar.second {
|
||||
width: 17rem;
|
||||
min-width: 17rem;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
"dm.accepted": "direct meowchat accepted",
|
||||
"no.invite.found": "you can't create a dm without invitation *hiss*",
|
||||
"unknown.error": "unknown error :o",
|
||||
"missing.keys": "cannot rotate nametags: some kitties don't have public nametags available yet. they must set up their identity.",
|
||||
"key.rotated": "meowchat keys have been successfully rotated! meow!",
|
||||
"forbidden": "you do not have pawmission to perform this action *hiss*",
|
||||
"dm.key.too.large": "meowchat key too large :o",
|
||||
"dm.key.not.found": "meowchat key not found :c",
|
||||
|
|
@ -125,6 +127,7 @@
|
|||
"desc.messages.loading": "loading meows...",
|
||||
"desc.no.dms": "no direct meowchats :c",
|
||||
"action.dm.opening": "opening meowchat...",
|
||||
"action.group.opening": "opening clowder...",
|
||||
"dm.open.failed": "failed to open meowchat :c",
|
||||
"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",
|
||||
|
|
@ -201,5 +204,58 @@
|
|||
"message.unpinned.by": "{0} unpinned a {1}.",
|
||||
"title.pinned.messages": "Pinned",
|
||||
"error.pinned.messages.empty": "No pinned meows",
|
||||
"title.message.lowercase": "meowsage"
|
||||
"title.message.lowercase": "meowsage",
|
||||
"action.rotate.keys": "Meowtate Keys",
|
||||
"already.in.group": "Meowready in group",
|
||||
"desc.key.rotation.info": "Meow need to purrotate meowcription keys to purrtinue sending meowsages in this group. Other meowmbers will autopurrtically receive the new key.",
|
||||
"desc.no.groups": "Meow are not in any groups yet.",
|
||||
"dm.read": "Purread",
|
||||
"empty.body": "Empty meowdy",
|
||||
"failed.create.group": "Failed to purreate group",
|
||||
"federation.not.implemented": "Meowderation not implemeowted",
|
||||
"group.not.found": "Group not meownd",
|
||||
"group.read": "Purread",
|
||||
"invalid.entry.name": "Invalid purrtry name",
|
||||
"invalid.key.index": "Invalid key index (purrlease purrefresh the page)",
|
||||
"invalid.msg.id": "Invalid meowsage ID",
|
||||
"invalid.payload": "Invalid purrload",
|
||||
"invalid.reaction.id": "Invalid purreaction ID",
|
||||
"invalid.user": "Invalid meowser",
|
||||
"joined": "Purroined",
|
||||
"key.rotation.required": "Key purrotation meowquired",
|
||||
"key.too.large": "Key too meowrge",
|
||||
"keys.public.mismatch": "Public keys mismemeow",
|
||||
"label.members": "MEOWMBERS",
|
||||
"message.deleted": "Meowsage purrleted",
|
||||
"message.not.found": "Meowsage not meownd",
|
||||
"message.pin.toggled": "Meowsage purrin purrggled",
|
||||
"message.reacted": "Meowsage purreacted",
|
||||
"message.reaction.removed": "Purreaction remeowed",
|
||||
"no.invite": "No meowvite",
|
||||
"not.a.member": "Not a meowmber",
|
||||
"not.in.group": "Not in purroup",
|
||||
"reaction.not.found": "Purreaction not meownd",
|
||||
"rotation.not.required": "Purrotation not meowquired",
|
||||
"storage.entry.updated": "Storage purrtry updated",
|
||||
"storage.limit.exceeded": "Storage limit exmeowded",
|
||||
"title.key.rotation": "Key Purrotation",
|
||||
"error:messages.decrypt.failed": "Depurryption meowled",
|
||||
"error.message.already.deleted": "Meowsage alpurrdy meowleted",
|
||||
"group.user.joined.by": "{0} purroined the group",
|
||||
"group.created.by": "Group purreated by {0}",
|
||||
"group.user.left.by": "{0} left the purroup",
|
||||
"action.group.fetch": "Meowtching groups",
|
||||
"group.created": "Group purreated",
|
||||
"group.user.joined": "Purroined group",
|
||||
"prompt.invite.username": "Enter username to invite meow:",
|
||||
"desc.failed.load.members": "Failed to load members meow",
|
||||
"prompt.are.you.sure": "Are you sure meow?",
|
||||
"title.unknown.group": "Unknown Group meow",
|
||||
"title.unknown": "Unknown meow",
|
||||
"en-us": "English meow",
|
||||
"pl-pl": "Polish meow",
|
||||
"en-cat": "Meowlish",
|
||||
"avatar.updated": "Avatar updated successfully",
|
||||
"profile.updated": "Profile updated successfully",
|
||||
"banner.updated": "Banner updated successfully"
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
"dm.accepted": "DM accepted",
|
||||
"no.invite.found": "You can't create a dm without invitation",
|
||||
"unknown.error": "Unknown error",
|
||||
"missing.keys": "Cannot rotate keys: Some members don't have public keys available yet. They must configure their identity.",
|
||||
"key.rotated": "Group keys have been successfully rotated.",
|
||||
"forbidden": "You do not have permission to perform this action",
|
||||
"dm.key.too.large": "DM key too large",
|
||||
"dm.key.not.found": "DM key not found",
|
||||
|
|
@ -124,6 +126,7 @@
|
|||
"desc.messages.loading": "Fetching messages...",
|
||||
"desc.no.dms": "No direct messages",
|
||||
"action.dm.opening": "Opening DM...",
|
||||
"action.group.opening": "Opening group...",
|
||||
"dm.open.failed": "Failed to open DM",
|
||||
"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.",
|
||||
|
|
@ -201,5 +204,58 @@
|
|||
"message.unpinned.by": "{0} unpinned a {1}.",
|
||||
"title.pinned.messages": "Pinned",
|
||||
"error.pinned.messages.empty": "No pinned messages",
|
||||
"title.message.lowercase": "message"
|
||||
"title.message.lowercase": "message",
|
||||
"action.rotate.keys": "Rotate Keys",
|
||||
"already.in.group": "Already in group",
|
||||
"desc.key.rotation.info": "You need to rotate encryption keys to continue sending messages in this group. Other members will automatically receive the new key.",
|
||||
"desc.no.groups": "You are not in any groups yet.",
|
||||
"dm.read": "Read",
|
||||
"empty.body": "Empty body",
|
||||
"failed.create.group": "Failed to create group",
|
||||
"federation.not.implemented": "Federation not implemented",
|
||||
"group.not.found": "Group not found",
|
||||
"group.read": "Read",
|
||||
"invalid.entry.name": "Invalid entry name",
|
||||
"invalid.key.index": "Invalid key index (please refresh the page)",
|
||||
"invalid.msg.id": "Invalid message ID",
|
||||
"invalid.payload": "Invalid payload",
|
||||
"invalid.reaction.id": "Invalid reaction ID",
|
||||
"invalid.user": "Invalid user",
|
||||
"joined": "Joined",
|
||||
"key.rotation.required": "Key rotation required",
|
||||
"key.too.large": "Key too large",
|
||||
"keys.public.mismatch": "Public keys mismatch",
|
||||
"label.members": "MEMBERS",
|
||||
"message.deleted": "Message deleted",
|
||||
"message.not.found": "Message not found",
|
||||
"message.pin.toggled": "Message pin toggled",
|
||||
"message.reacted": "Message reacted",
|
||||
"message.reaction.removed": "Reaction removed",
|
||||
"no.invite": "No invite",
|
||||
"not.a.member": "Not a member",
|
||||
"not.in.group": "Not in group",
|
||||
"reaction.not.found": "Reaction not found",
|
||||
"rotation.not.required": "Rotation not required",
|
||||
"storage.entry.updated": "Storage entry updated",
|
||||
"storage.limit.exceeded": "Storage limit exceeded",
|
||||
"title.key.rotation": "Key Rotation",
|
||||
"error:messages.decrypt.failed": "Decryption failed",
|
||||
"error.message.already.deleted": "Message already deleted",
|
||||
"group.user.joined.by": "{0} joined the group",
|
||||
"group.created.by": "Group created by {0}",
|
||||
"group.user.left.by": "{0} left the group",
|
||||
"action.group.fetch": "Fetching groups",
|
||||
"group.created": "Group created",
|
||||
"group.user.joined": "Joined group",
|
||||
"prompt.invite.username": "Enter username to invite:",
|
||||
"desc.failed.load.members": "Failed to load members",
|
||||
"prompt.are.you.sure": "Are you sure?",
|
||||
"title.unknown.group": "Unknown Group",
|
||||
"title.unknown": "Unknown",
|
||||
"success:banner.updated": "Banner updated successfully",
|
||||
"success:profile.updated": "Profile updated successfully",
|
||||
"success:avatar.updated": "Avatar updated successfully",
|
||||
"en-us": "English",
|
||||
"pl-pl": "Polish",
|
||||
"en-cat": "Meowlish"
|
||||
}
|
||||
|
|
@ -124,6 +124,7 @@
|
|||
"desc.messages.loading": "Pobieranie wiadomości...",
|
||||
"desc.no.dms": "Brak wiadomości prywatnych",
|
||||
"action.dm.opening": "Otwieranie rozmowy...",
|
||||
"action.group.opening": "Otwieranie grupy...",
|
||||
"dm.open.failed": "Nie udało się otworzyć rozmowy",
|
||||
"keys.local.server.mismatch": "To urządzenie ma inne klucze szyfrowania niż serwer. Użyj urządzenia, na którym zalogowano się po raz pierwszy, lub przywróć klucze z kopii zapasowej.",
|
||||
"keys.server.decrypt.failed": "Nie udało się odszyfrować kluczy konta przy użyciu tego hasła.",
|
||||
|
|
@ -201,5 +202,60 @@
|
|||
"message.unpinned.by": "{0} odpiął {1}.",
|
||||
"title.pinned.messages": "Przypięte",
|
||||
"error.pinned.messages.empty": "Brak przypiętych wiadomości",
|
||||
"title.message.lowercase": "wiadomość"
|
||||
"title.message.lowercase": "wiadomość",
|
||||
"action.rotate.keys": "Rotuj Klucze",
|
||||
"already.in.group": "Już w grupie",
|
||||
"desc.key.rotation.info": "Musisz zrotować klucze szyfrujące, aby kontynuować wysyłanie wiadomości w tej grupie. Inni członkowie automatycznie otrzymają nowy klucz.",
|
||||
"desc.no.groups": "Nie jesteś jeszcze w żadnej grupie.",
|
||||
"dm.read": "Przeczytano",
|
||||
"empty.body": "Pusta zawartość",
|
||||
"failed.create.group": "Nie udało się utworzyć grupy",
|
||||
"federation.not.implemented": "Federacja niezaimplementowana",
|
||||
"group.not.found": "Nie znaleziono grupy",
|
||||
"group.read": "Przeczytano",
|
||||
"invalid.entry.name": "Nieprawidłowa nazwa wpisu",
|
||||
"invalid.key.index": "Nieprawidłowy indeks klucza (odśwież stronę)",
|
||||
"invalid.msg.id": "Nieprawidłowe ID wiadomości",
|
||||
"invalid.payload": "Nieprawidłowy ładunek",
|
||||
"invalid.reaction.id": "Nieprawidłowe ID reakcji",
|
||||
"invalid.user": "Nieprawidłowy użytkownik",
|
||||
"joined": "Dołączono",
|
||||
"key.rotation.required": "Wymagana rotacja kluczy",
|
||||
"key.too.large": "Klucz zbyt duży",
|
||||
"keys.public.mismatch": "Niezgodność kluczy publicznych",
|
||||
"label.members": "CZŁONKOWIE",
|
||||
"message.deleted": "Wiadomość usunięta",
|
||||
"message.not.found": "Nie znaleziono wiadomości",
|
||||
"message.pin.toggled": "Zmieniono przypięcie wiadomości",
|
||||
"message.reacted": "Dodano reakcję",
|
||||
"message.reaction.removed": "Reakcja usunięta",
|
||||
"no.invite": "Brak zaproszenia",
|
||||
"not.a.member": "Nie jesteś członkiem",
|
||||
"not.in.group": "Nie jesteś w grupie",
|
||||
"reaction.not.found": "Nie znaleziono reakcji",
|
||||
"rotation.not.required": "Rotacja niewymagana",
|
||||
"storage.entry.updated": "Zaktualizowano wpis pamięci",
|
||||
"storage.limit.exceeded": "Przekroczono limit pamięci",
|
||||
"title.key.rotation": "Rotacja kluczy",
|
||||
"missing.keys": "Brakujące klucze",
|
||||
"error:messages.decrypt.failed": "Deszyfrowanie nie powiodło się",
|
||||
"error.message.already.deleted": "Wiadomość została już usunięta",
|
||||
"group.user.joined.by": "{0} dołączył do grupy",
|
||||
"group.created.by": "Grupa utworzona przez {0}",
|
||||
"group.user.left.by": "{0} opuścił grupę",
|
||||
"action.group.fetch": "Pobieranie grup",
|
||||
"group.created": "Utworzono grupę",
|
||||
"group.user.joined": "Dołączono do grupy",
|
||||
"prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:",
|
||||
"desc.failed.load.members": "Nie udało się załadować członków",
|
||||
"prompt.are.you.sure": "Czy na pewno?",
|
||||
"title.unknown.group": "Nieznana grupa",
|
||||
"title.unknown": "Nieznany",
|
||||
"key.rotated": "Klucze grupy zostały pomyślnie zrotowane.",
|
||||
"success:banner.updated": "Baner został pomyślnie zaktualizowany",
|
||||
"success:profile.updated": "Profil został pomyślnie zaktualizowany",
|
||||
"success:avatar.updated": "Awatar został pomyślnie zaktualizowany",
|
||||
"en-us": "Angielski",
|
||||
"pl-pl": "Polski",
|
||||
"en-cat": "Koci (Meowlish)"
|
||||
}
|
||||
|
|
@ -119,6 +119,25 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rotation-popup-container" class="profile-popup-container" style="display: none; z-index: 9999;">
|
||||
<div class="profile-popup-bg" onclick="closeRotationPopup()"></div>
|
||||
<div class="profile-popup-content">
|
||||
<button class="profile-popup-close-btn" onclick="closeRotationPopup()">
|
||||
<script>
|
||||
document.currentScript.insertAdjacentHTML('beforebegin', ICONS.close());
|
||||
document.currentScript.remove();
|
||||
</script>
|
||||
</button>
|
||||
<div id="profile-popup-inner" style="padding: 2rem; display: flex; flex-direction: column; align-items: center; gap: 1rem;">
|
||||
<div style="font-size: 1.2rem; font-weight: 500; text-align: center;"><blah>title.key.rotation</blah></div>
|
||||
<div style="width: 100%; text-align: center; color: var(--text-color); opacity: 0.8; margin-top: 1rem; margin-bottom: 1rem;" id="rotation-popup-desc">
|
||||
<!-- content injected dynamically -->
|
||||
</div>
|
||||
<button class="profile-action-btn" onclick="executeKeyRotation()"><blah>action.rotate.keys</blah></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -208,8 +227,59 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function createGroup() {
|
||||
async function refreshGroups(waittime = 0, showLoading = true) {
|
||||
try {
|
||||
if (showLoading) showAction("action.group.fetch", "grouprefresh");
|
||||
await delay(waittime);
|
||||
if (window.cachedGroups && typeof renderGroups === 'function') {
|
||||
await renderGroups(window.cachedGroups);
|
||||
}
|
||||
let res = await fetchEncrypted("user/group/list");
|
||||
window.cachedGroups = res;
|
||||
if (typeof renderGroups === 'function') {
|
||||
await renderGroups(res);
|
||||
}
|
||||
if (showLoading) clearAction("grouprefresh");
|
||||
}
|
||||
catch (e) {
|
||||
if (showLoading) clearAction("grouprefresh");
|
||||
showBlahNotification("error:group.refresh.failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function createGroup() {
|
||||
try {
|
||||
let groupName = document.getElementById("creategroup-name").value;
|
||||
if (!groupName || groupName.trim() === "") {
|
||||
showBlahNotification("error:group.name.empty");
|
||||
return;
|
||||
}
|
||||
|
||||
showAction("action.group.creating", "group.action");
|
||||
|
||||
await ensureUserKeys();
|
||||
const roomKey = randomRoomKey256Chars();
|
||||
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupName,
|
||||
string2: selfWrapped
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/create", payload);
|
||||
clearAction("group.action");
|
||||
|
||||
if (res.startsWith("success:")) {
|
||||
showBlahNotification("success:group.created");
|
||||
await refreshGroups(200, false);
|
||||
} else {
|
||||
showBlahNotification(res || "error:something.wrong");
|
||||
}
|
||||
} catch (e) {
|
||||
clearAction("group.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="gifuct.js"></script>
|
||||
|
|
|
|||
702
webroot/main.js
702
webroot/main.js
|
|
@ -367,6 +367,171 @@ async function ensureDmRoomKey(dmId) {
|
|||
|
||||
window.ensureDmRoomKey = ensureDmRoomKey;
|
||||
|
||||
window.groupKeysCache = {};
|
||||
|
||||
async function getGroupKey(groupId, keyIndex) {
|
||||
if (!window.groupKeysCache[groupId]) {
|
||||
window.groupKeysCache[groupId] = {};
|
||||
}
|
||||
if (window.groupKeysCache[groupId][keyIndex]) {
|
||||
return window.groupKeysCache[groupId][keyIndex];
|
||||
}
|
||||
|
||||
let payload = JSON.stringify({string1: groupId, string2: keyIndex.toString()});
|
||||
let res = await fetchEncrypted("group/key/get", payload);
|
||||
if (!res || res.startsWith("error:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let decryptedKey;
|
||||
if (res.startsWith("SETUP:")) {
|
||||
const rest = res.substring("SETUP:".length);
|
||||
const parts = rest.split(";");
|
||||
if (parts.length < 2) throw new Error("error:group.key.setup.malformed");
|
||||
|
||||
const otherPublicJson = parts[0];
|
||||
const setupPayloadJson = parts.slice(1).join(";");
|
||||
const otherPublic = JSON.parse(otherPublicJson);
|
||||
const setupPayload = JSON.parse(setupPayloadJson);
|
||||
|
||||
const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem);
|
||||
decryptedKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
|
||||
} else {
|
||||
decryptedKey = await unwrapRoomKeyForSelf(res);
|
||||
}
|
||||
|
||||
window.groupKeysCache[groupId][keyIndex] = decryptedKey;
|
||||
return decryptedKey;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeRotationPopup() {
|
||||
let popupContainer = document.getElementById("rotation-popup-container");
|
||||
if(popupContainer) {
|
||||
popupContainer.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
popupContainer.style.display = "none";
|
||||
}, 200);
|
||||
}
|
||||
clearAction("key_rotation");
|
||||
}
|
||||
|
||||
let pendingRotationMessagePayload = null;
|
||||
|
||||
async function showRotationPopup(groupId, originalMessagePayload) {
|
||||
pendingRotationMessagePayload = originalMessagePayload;
|
||||
showAction("action.key.rotation", "key_rotation");
|
||||
|
||||
try {
|
||||
let membersRaw = await fetchEncrypted("group/members", groupId);
|
||||
if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw);
|
||||
|
||||
let members = JSON.parse(membersRaw);
|
||||
|
||||
let desc = document.getElementById("rotation-popup-desc");
|
||||
desc.innerHTML = `<blah>desc.key.rotation.info</blah><br><br><b>${members.length} <blah>label.members</blah></b>`;
|
||||
let blahTags = desc.getElementsByTagName("blah");
|
||||
for (let i = 0; i < blahTags.length; i++) {
|
||||
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
|
||||
}
|
||||
|
||||
let popupContainer = document.getElementById("rotation-popup-container");
|
||||
popupContainer.style.display = "flex";
|
||||
popupContainer.offsetHeight; // trigger reflow
|
||||
popupContainer.classList.add("show");
|
||||
} catch(e) {
|
||||
clearAction("key_rotation");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function executeKeyRotation() {
|
||||
closeRotationPopup();
|
||||
showAction("action.key.rotating", "key_rotation_exec");
|
||||
|
||||
try {
|
||||
let groupId = currentDmId;
|
||||
|
||||
let membersRaw = await fetchEncrypted("group/members", groupId);
|
||||
if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw);
|
||||
let members = JSON.parse(membersRaw);
|
||||
|
||||
let roomKey = randomRoomKey256Chars();
|
||||
let keysDict = {};
|
||||
let myPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${window.id}`);
|
||||
|
||||
for(let memberId of members) {
|
||||
let pureId = memberId.split(";")[0];
|
||||
if(pureId === window.id) {
|
||||
keysDict[memberId] = await wrapRoomKeyForSelf(roomKey);
|
||||
} else {
|
||||
let publicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${pureId}`);
|
||||
if(publicKeysRaw && publicKeysRaw.length > 10) {
|
||||
let publicKeys = JSON.parse(publicKeysRaw);
|
||||
let hybrid = await deriveHybridKeyToUser(publicKeys);
|
||||
let encForMember = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes);
|
||||
let setupPayloadJson = JSON.stringify({
|
||||
v: 1,
|
||||
alg: "hybrid-x25519-mlkem768",
|
||||
ctMlKem: hybrid.ciphertextMlKemBase64,
|
||||
enc: encForMember
|
||||
});
|
||||
keysDict[memberId] = `SETUP:${myPublicKeysRaw};${setupPayloadJson}`;
|
||||
} else {
|
||||
throw new Error("error:missing.keys");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let payload = JSON.stringify({
|
||||
groupId: groupId,
|
||||
keys: keysDict
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/key/rotate", payload);
|
||||
if(res && res.startsWith("error:")) {
|
||||
throw new Error(res);
|
||||
}
|
||||
|
||||
// Success!
|
||||
let newIndex = res.replace("success:", "");
|
||||
window.currentGroupKeyIndex = newIndex;
|
||||
|
||||
if(!window.groupKeysCache[groupId]) window.groupKeysCache[groupId] = {};
|
||||
window.groupKeysCache[groupId][newIndex] = roomKey;
|
||||
|
||||
clearAction("key_rotation_exec");
|
||||
showBlahNotification("success:key.rotated");
|
||||
|
||||
// Retry sending the message!
|
||||
if(pendingRotationMessagePayload) {
|
||||
let p = pendingRotationMessagePayload;
|
||||
p.string5 = newIndex;
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(roomKey));
|
||||
p.string2 = await encryptAesGcmToBase64(p.rawPlainContent, dmKeyBytes); // we need to save rawPlainContent for retry
|
||||
|
||||
showAction("action.message.sending", "msgsending");
|
||||
let resSend = await fetchEncrypted("group/message/send", JSON.stringify(p));
|
||||
clearAction("msgsending");
|
||||
|
||||
if(resSend && resSend.startsWith("error:")) {
|
||||
showBlahNotification(resSend);
|
||||
} else if(resSend && resSend.startsWith("success:")) {
|
||||
loadDmMessages(currentDmId);
|
||||
}
|
||||
}
|
||||
pendingRotationMessagePayload = null;
|
||||
|
||||
} catch(e) {
|
||||
clearAction("key_rotation_exec");
|
||||
console.error(e);
|
||||
showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function encrypt(plainText, keyBytes) {
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const encoder = new TextEncoder();
|
||||
|
|
@ -857,6 +1022,18 @@ async function getAvatarUrl(id, username) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getGroupAvatarUrl(groupId, groupName) {
|
||||
try {
|
||||
let pfpUrl = `${url}/group/storage/public/getentry?id=${groupId}&e=larp.profile.pfp`;
|
||||
if ((await fetchAsync(pfpUrl)) == "") {
|
||||
throw Error();
|
||||
}
|
||||
return pfpUrl;
|
||||
} catch (e) {
|
||||
return createAvatarSvg(groupName);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoadingStatus(message) {
|
||||
loadingStatus.innerHTML = processBlah(message);
|
||||
}
|
||||
|
|
@ -1743,6 +1920,71 @@ function setActiveRoombarItem(itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
async function acceptGroupInvite(groupId, inviterId) {
|
||||
try {
|
||||
showAction("action.invite.accepting", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: inviterId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/join", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:joined");
|
||||
|
||||
// Refresh invites and groups
|
||||
window.cachedInvites = {'received': null, 'sent': null};
|
||||
loadInvites("received");
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function declineGroupInvite(groupId, inviterId) {
|
||||
try {
|
||||
showAction("action.invite.declining", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: inviterId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/invite/decline", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:invite.declined");
|
||||
|
||||
window.cachedInvites = {'received': null, 'sent': null};
|
||||
loadInvites("received");
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeGroupInvite(groupId, targetId) {
|
||||
try {
|
||||
showAction("action.invite.revoking", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: targetId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/invite/revoke", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:invite.revoked");
|
||||
|
||||
window.cachedInvites = {'received': null, 'sent': null};
|
||||
loadInvites("sent");
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function renderInvites(res, type) {
|
||||
let container = document.getElementById("invites-container");
|
||||
if (!container) return;
|
||||
|
|
@ -1781,7 +2023,7 @@ async function renderInvites(res, type) {
|
|||
}
|
||||
let onlyId = id.split(":")[0];
|
||||
let username = await fetchAsync(`${url}/idtoname?id=${id}`);
|
||||
if (!(username && username.trim() !== "")) return;
|
||||
if (!(username && username.trim() !== "")) continue;
|
||||
count++;
|
||||
|
||||
let pfp = await getAvatarUrl(id, username);
|
||||
|
|
@ -1789,8 +2031,9 @@ async function renderInvites(res, type) {
|
|||
let actions = "";
|
||||
let desc = "";
|
||||
|
||||
let displayName = username.split(':')[0] || onlyId;
|
||||
if (type === "received") {
|
||||
desc = `:desc.invite.dm.received:${username};${onlyId}`;
|
||||
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-green);" onclick="acceptInvite('${id}')">
|
||||
${ICONS.check({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
|
|
@ -1800,7 +2043,7 @@ async function renderInvites(res, type) {
|
|||
</button>
|
||||
`;
|
||||
} else {
|
||||
desc = `:desc.invite.dm.sent:${username};${onlyId}`;
|
||||
desc = `:desc.invite.dm.sent:${displayName};${onlyId}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-red);" onclick="revokeInvite('${id}')">
|
||||
${ICONS.close({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
|
|
@ -1809,12 +2052,70 @@ async function renderInvites(res, type) {
|
|||
}
|
||||
|
||||
let entry = invitesEntry
|
||||
.replaceAll("{username}", username.split(':')[0])
|
||||
.replaceAll("{username}", displayName)
|
||||
.replaceAll("{pfp}", pfp)
|
||||
.replaceAll("{desc}", desc)
|
||||
.replaceAll("{desc}", `<blah>${desc}</blah>`)
|
||||
.replaceAll("{actions}", actions)
|
||||
.replaceAll("{onclick_attr}", `onclick="openProfile('${id}')" style="cursor: pointer;"`)
|
||||
.replaceAll("{id}", id);
|
||||
|
||||
html += entry;
|
||||
} else if (invite.type === "group") {
|
||||
let groupId = invite.id;
|
||||
let targetId = (type === "received") ? invite.sender : invite.receiver;
|
||||
|
||||
if (!targetId.includes(":")) {
|
||||
targetId += `:${host}`;
|
||||
}
|
||||
let onlyId = targetId.split(":")[0];
|
||||
let username = await fetchAsync(`${url}/idtoname?id=${targetId}`);
|
||||
if (!(username && username.trim() !== "")) continue;
|
||||
|
||||
// Fetch group name
|
||||
let groupInfoStr = await fetchEncrypted("group/info", groupId);
|
||||
let groupName = processBlah("title.unknown.group");
|
||||
if (groupInfoStr && !groupInfoStr.startsWith("error")) {
|
||||
let groupInfo = JSON.parse(groupInfoStr);
|
||||
groupName = groupInfo.string2;
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
let pfp = await getGroupAvatarUrl(groupId, groupName);
|
||||
let actions = "";
|
||||
let desc = "";
|
||||
|
||||
let displayName = username.split(':')[0] || onlyId;
|
||||
let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`;
|
||||
let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`;
|
||||
|
||||
if (type === "received") {
|
||||
desc = `:desc.invite.group.received:${val1};${val2}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-green);" onclick="acceptGroupInvite('${groupId}', '${targetId}')">
|
||||
${ICONS.check({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
</button>
|
||||
<button class="icon-button" style="background-color: var(--big-red);" onclick="declineGroupInvite('${groupId}', '${targetId}')">
|
||||
${ICONS.close({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
desc = `:desc.invite.group.sent:${val1};${val2}`;
|
||||
actions = `
|
||||
<button class="icon-button" style="background-color: var(--big-red);" onclick="revokeGroupInvite('${groupId}', '${targetId}')">
|
||||
${ICONS.close({ fill: "#fff", width: "1.5rem", height: "1.5rem" })}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
let entry = invitesEntry
|
||||
.replaceAll("{username}", groupName)
|
||||
.replaceAll("{pfp}", pfp)
|
||||
.replaceAll("{desc}", `<blah>${desc}</blah>`)
|
||||
.replaceAll("{actions}", actions)
|
||||
.replaceAll("{onclick_attr}", "")
|
||||
.replaceAll("{id}", groupId);
|
||||
|
||||
html += entry;
|
||||
}
|
||||
}
|
||||
|
|
@ -1996,7 +2297,10 @@ async function gotoHome(splash = true) {
|
|||
switchRoomsBar("title.home", homeRoomBar);
|
||||
setActiveRoombarItem(null);
|
||||
setupWebSocket();
|
||||
await refreshDms(210);
|
||||
await Promise.all([
|
||||
refreshDms(210),
|
||||
(typeof refreshGroups === 'function' ? refreshGroups(210) : Promise.resolve())
|
||||
]);
|
||||
}
|
||||
|
||||
function setActiveSidebarIndicator(element, isTemporary = false) {
|
||||
|
|
@ -2210,6 +2514,67 @@ async function renderDms(res) {
|
|||
}
|
||||
}
|
||||
|
||||
async function renderGroups(res) {
|
||||
let container = document.getElementById("groups-list");
|
||||
if (!container) return;
|
||||
|
||||
if (res && res !== "") {
|
||||
let parsed = JSON.parse(res);
|
||||
let groups = parsed.groups || {};
|
||||
let groupEntries = Object.entries(groups);
|
||||
|
||||
//Sort by timestamp descending
|
||||
groupEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
|
||||
|
||||
let html = "";
|
||||
let count = 0;
|
||||
|
||||
for (let [groupId, groupData] of groupEntries) {
|
||||
let isRead = groupData.string3 === "true";
|
||||
let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";
|
||||
|
||||
let infoRes = await fetchEncrypted("group/info", groupId);
|
||||
if (infoRes && infoRes.startsWith("{")) {
|
||||
let info = JSON.parse(infoRes);
|
||||
let groupName = info.string2;
|
||||
|
||||
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
|
||||
let pfp = await getGroupAvatarUrl(groupId, groupName);
|
||||
|
||||
html += `
|
||||
<button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')">
|
||||
<img src="${pfp}" class="room-pfp">
|
||||
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||
<span class="room-name" style="${nameStyle}">${groupName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
container.innerHTML = html;
|
||||
container.classList.remove("empty");
|
||||
} else {
|
||||
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.groups</blah></p>`;
|
||||
container.classList.add("empty");
|
||||
let blahTags = container.getElementsByTagName("blah");
|
||||
for (let i = 0; i < blahTags.length; i++) {
|
||||
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.groups</blah></p>`;
|
||||
container.classList.add("empty");
|
||||
let blahTags = container.getElementsByTagName("blah");
|
||||
for (let i = 0; i < blahTags.length; i++) {
|
||||
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentDmId = null;
|
||||
var currentDmKey = null; // AES key for encrypting/decrypting messages
|
||||
var dmUsernameCache = {};
|
||||
|
|
@ -2263,30 +2628,131 @@ async function generateDmMembersHtml(targetId, targetUsername) {
|
|||
`;
|
||||
}
|
||||
|
||||
async function openDm(dmId, username, targetId) {
|
||||
async function inviteToGroupPrompt(groupId) {
|
||||
let targetUsername = prompt(processBlah("prompt.invite.username"));
|
||||
if (!targetUsername) return;
|
||||
|
||||
if (!targetUsername.includes(":")) {
|
||||
targetUsername += `:${host}`;
|
||||
}
|
||||
|
||||
let idRes = await fetchAsync(`${url}/nametoid?u=${targetUsername}`);
|
||||
if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") {
|
||||
showBlahNotification("error:user.not.found");
|
||||
return;
|
||||
}
|
||||
let targetId = idRes.trim();
|
||||
|
||||
try {
|
||||
showAction("action.invite.sending", "invite.action");
|
||||
|
||||
let payload = JSON.stringify({
|
||||
string1: groupId,
|
||||
string2: targetId
|
||||
});
|
||||
|
||||
let res = await fetchEncrypted("group/invite", payload);
|
||||
clearAction("invite.action");
|
||||
showBlahNotification(res || "success:invite.sent");
|
||||
} catch (e) {
|
||||
clearAction("invite.action");
|
||||
showBlahNotification("error:something.wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGroupMembersHtml(groupId, groupName) {
|
||||
let membersJson = await fetchEncrypted("group/members", groupId);
|
||||
if (!membersJson || membersJson.startsWith("error")) {
|
||||
return `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.failed.load.members</blah></div>`;
|
||||
}
|
||||
|
||||
let members = JSON.parse(membersJson);
|
||||
let html = `
|
||||
<div class="list-container no-flex-grow" style="padding: 0 0.5rem; width: 100%;">
|
||||
<div class="sidebar-section-header" style="margin-top: 0.5rem; margin-bottom: 0.5rem; width: 100%; display: flex; align-items: center;">
|
||||
<span style="padding-left: 0.6rem; font-size: 0.8rem; font-weight: bold; opacity: 0.5; text-transform: uppercase;"><blah>title.members</blah></span>
|
||||
<div class="flex-spacer"></div>
|
||||
<button class="topbar-action-btn" onclick="inviteToGroupPrompt('${groupId}')" style="margin-right: 0.5rem; width: 1.2rem; height: 1.2rem; display: flex; align-items: center; justify-content: center; opacity: 0.7;">
|
||||
${ICONS.plus({ width: "1rem", height: "1rem" })}
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
for (let memberId of members) {
|
||||
let memberUsername = await fetchAsync(`${url}/idtoname?id=${memberId}`);
|
||||
if (!memberUsername || memberUsername.startsWith("error")) continue;
|
||||
|
||||
let memberPfp = await getAvatarUrl(memberId, memberUsername);
|
||||
let memberDisplayName = memberUsername.split(':')[0];
|
||||
|
||||
html += `
|
||||
<button class="room-entry" onclick="openProfile('${memberId}')" style="margin-bottom: 0;">
|
||||
<img src="${memberPfp}" class="room-pfp">
|
||||
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||
<span class="room-name">${memberDisplayName}</span>
|
||||
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${memberUsername}</span>
|
||||
</div>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
async function openDm(dmId, username, targetId, type = 'dms') {
|
||||
try {
|
||||
showAction("action.dm.opening", "dmopen");
|
||||
|
||||
markDmAsRead(dmId);
|
||||
if (type === 'groups') {
|
||||
markDmAsRead(dmId, 'groups');
|
||||
} else {
|
||||
markDmAsRead(dmId, 'dms');
|
||||
}
|
||||
|
||||
if (type === 'dms') {
|
||||
currentDmKey = await ensureDmRoomKey(dmId);
|
||||
if (!currentDmKey) {
|
||||
throw new Error("Missing DM room key");
|
||||
}
|
||||
} else {
|
||||
let infoPayload = await fetchEncrypted("group/info", dmId);
|
||||
if (infoPayload && !infoPayload.startsWith("error:")) {
|
||||
let info = JSON.parse(infoPayload);
|
||||
window.currentGroupKeyIndex = info.string4 || "0";
|
||||
} else {
|
||||
window.currentGroupKeyIndex = "0";
|
||||
}
|
||||
}
|
||||
|
||||
if (currentDmId !== dmId) {
|
||||
currentRoomContentTitle = "";
|
||||
}
|
||||
currentDmId = dmId;
|
||||
window.currentRoomType = type; // Save the type globally
|
||||
loadedMessages = {};
|
||||
oldestLoadedMsgId = null;
|
||||
isLoadingOlderMessages = false;
|
||||
|
||||
let iconHtml = "";
|
||||
if (type === 'dms') {
|
||||
let pfp = await getAvatarUrl(targetId, username);
|
||||
let iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||
iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||
} else {
|
||||
let pfp = await getGroupAvatarUrl(dmId, username);
|
||||
iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||
}
|
||||
|
||||
if (currentRoomsBarTitle !== "title.home") {
|
||||
gotoHome(false);
|
||||
}
|
||||
|
||||
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
||||
let membersHtml = await generateDmMembersHtml(targetId, username);
|
||||
|
||||
let membersHtml = "";
|
||||
if (type === 'dms') {
|
||||
membersHtml = await generateDmMembersHtml(targetId, username);
|
||||
} else {
|
||||
membersHtml = await generateGroupMembersHtml(dmId, username);
|
||||
}
|
||||
await switchDetailsContent("title.details", membersHtml);
|
||||
|
||||
let msgContainer = document.getElementById("chat-messages");
|
||||
|
|
@ -2326,7 +2792,8 @@ async function openDm(dmId, username, targetId) {
|
|||
async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
||||
try {
|
||||
let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset});
|
||||
let res = await fetchEncrypted("dm/messages/get", payload);
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/messages/get" : "dm/messages/get";
|
||||
let res = await fetchEncrypted(endpoint, payload);
|
||||
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
|
|
@ -2389,9 +2856,26 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let decrypted = false;
|
||||
if (msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
keyToUse = await getGroupKey(currentDmId, msg.key);
|
||||
if (keyToUse) {
|
||||
let mK = parseInt(msg.key);
|
||||
let cK = parseInt(window.currentGroupKeyIndex || "0");
|
||||
if (!isNaN(mK) && !isNaN(cK) && mK > cK) {
|
||||
window.currentGroupKeyIndex = msg.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyToUse) {
|
||||
content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
|
||||
decrypted = false;
|
||||
} else {
|
||||
let keyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
content = await decryptAesGcmFromBase64(content, keyBytes);
|
||||
decrypted = true;
|
||||
}
|
||||
} catch (e) {
|
||||
content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
|
||||
decrypted = false;
|
||||
|
|
@ -2423,7 +2907,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let safeType = encodeURIComponent(att.type).replace(/'/g, "%27");
|
||||
let safeName = encodeURIComponent(att.name).replace(/'/g, "%27");
|
||||
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'))">
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'), '${msg.key || "0"}')">
|
||||
<div style="padding: 1rem; text-align: center; color: var(--text-color);">
|
||||
${iconSvg}
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; word-break: break-all;">${escapeHtml(displayName)}</div>
|
||||
|
|
@ -2454,8 +2938,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
content = processBlah(content);
|
||||
|
||||
if (msg.type && msg.type.startsWith("larp.info")) {
|
||||
if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin") {
|
||||
let authorName = "Unknown";
|
||||
if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin" || msg.type === "larp.info.join" || msg.type === "larp.info.leave" || msg.type === "larp.info.create") {
|
||||
let authorName = processBlah("title.unknown");
|
||||
if (msg.author && msg.author !== "0") {
|
||||
if (!dmUsernameCache[msg.author]) {
|
||||
let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`);
|
||||
|
|
@ -2471,14 +2955,22 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
}
|
||||
|
||||
let isPinned = msg.type === "larp.info.pin";
|
||||
let actionStr = isPinned ? "message.pinned.by" : "message.unpinned.by";
|
||||
let actionStr = "";
|
||||
if (msg.type === "larp.info.pin") actionStr = "message.pinned.by";
|
||||
else if (msg.type === "larp.info.unpin") actionStr = "message.unpinned.by";
|
||||
else if (msg.type === "larp.info.join") actionStr = "group.user.joined.by";
|
||||
else if (msg.type === "larp.info.leave") actionStr = "group.user.left.by";
|
||||
else if (msg.type === "larp.info.create") actionStr = "group.created.by";
|
||||
|
||||
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
|
||||
|
||||
let textStr = processBlah(actionStr);
|
||||
if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) {
|
||||
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg);
|
||||
} else {
|
||||
content = textStr.replace("{0}", clickableAuthor);
|
||||
}
|
||||
}
|
||||
html += `<div style="text-align: center; opacity: 0.5; padding: 1rem; font-size: 0.9rem; word-break: break-word; overflow-wrap: anywhere;">${content}</div>`;
|
||||
lastAuthor = null;
|
||||
|
|
@ -2501,7 +2993,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
}
|
||||
lastAuthor = msg.author;
|
||||
|
||||
let authorName = "Unknown";
|
||||
let authorName = processBlah("title.unknown");
|
||||
let pfp = "assets/default_avatar.png";
|
||||
if (msg.author !== "0") {
|
||||
if (!dmUsernameCache[msg.author]) {
|
||||
|
|
@ -2618,8 +3110,15 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
|
|||
let rxVal = rx.r;
|
||||
if (msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, msg.key);
|
||||
if (k) keyToUse = k;
|
||||
}
|
||||
if (keyToUse) {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false, myReactionId: null};
|
||||
|
|
@ -2839,8 +3338,8 @@ function setupChatScrollListener() {
|
|||
|
||||
|
||||
|
||||
async function downloadAndRenderAttachment(container, msgId, attId, type, name) {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
async function downloadAndRenderAttachment(container, msgId, attId, type, name, keyIndex = "0") {
|
||||
if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return;
|
||||
if (!container) return;
|
||||
|
||||
if (container.dataset.loaded === "true") {
|
||||
|
|
@ -2860,10 +3359,16 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name)
|
|||
container.innerHTML = `<div style="padding: 1rem; color: var(--text-color);">${ICONS.spinner()}</div>`;
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, keyIndex);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
|
||||
let res = await fetch(`${url}/dm/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, {
|
||||
let endpointType = window.currentRoomType === 'groups' ? "group" : "dm";
|
||||
let res = await fetch(`${url}/${endpointType}/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, {
|
||||
headers: {
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
}
|
||||
|
|
@ -2953,7 +3458,7 @@ function renderAttachmentsPreview() {
|
|||
}
|
||||
|
||||
async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return;
|
||||
|
||||
let input = document.getElementById("chat-input");
|
||||
let content = input.value.trim();
|
||||
|
|
@ -2975,7 +3480,13 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
}
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, window.currentGroupKeyIndex || "0");
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||
|
||||
let attachmentsPayload = [];
|
||||
|
|
@ -3036,6 +3547,11 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
string4: replyingToMsgId || ""
|
||||
};
|
||||
|
||||
if (window.currentRoomType === 'groups') {
|
||||
msgPayload.string5 = window.currentGroupKeyIndex || "0";
|
||||
msgPayload.rawPlainContent = content; // for retry
|
||||
}
|
||||
|
||||
input.value = "";
|
||||
input.style.height = '2.5rem';
|
||||
sendbutton.style.height = '2.5rem';
|
||||
|
|
@ -3049,9 +3565,14 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
|
||||
showAction("action.message.sending", "msgsending");
|
||||
|
||||
let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
|
||||
let res = await fetchEncrypted(window.currentRoomType === 'groups' ? "group/message/send" : "dm/message/send", JSON.stringify(msgPayload));
|
||||
clearAction("msgsending");
|
||||
if (res && res.startsWith("error:")) {
|
||||
|
||||
if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') {
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
showRotationPopup(currentDmId, msgPayload);
|
||||
} else if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
|
|
@ -3073,7 +3594,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
type: "larp.text",
|
||||
content: encryptedContent,
|
||||
attachment: finalAttPayload,
|
||||
key: "0",
|
||||
key: window.currentRoomType === 'groups' ? (window.currentGroupKeyIndex || "0") : "0",
|
||||
pervious: "",
|
||||
responded: replyingToMsgId || "",
|
||||
reactions: ""
|
||||
|
|
@ -3148,15 +3669,16 @@ function setupWebSocket() {
|
|||
|
||||
appWebSocket.onmessage = (event) => {
|
||||
let data = event.data;
|
||||
if (data.startsWith("dm_message:")) {
|
||||
if (data.startsWith("dm_message:") || data.startsWith("group_message:")) {
|
||||
let parts = data.split(":");
|
||||
let msgTypeRoom = data.startsWith("dm_message:") ? "dms" : "groups";
|
||||
let msgDmId = parts[1];
|
||||
let msgType = parts[2] || "unread";
|
||||
|
||||
if (msgType === "unread") {
|
||||
updateDmListUI(msgDmId, false, Date.now().toString());
|
||||
updateDmListUI(msgDmId, false, msgTypeRoom, Date.now().toString());
|
||||
} else if (msgType === "read") {
|
||||
updateDmListUI(msgDmId, true, Date.now().toString());
|
||||
updateDmListUI(msgDmId, true, msgTypeRoom, Date.now().toString());
|
||||
} else if (msgType === "update") {
|
||||
// do nothing to UI sorting/read state
|
||||
}
|
||||
|
|
@ -3246,7 +3768,8 @@ async function togglePinMessage(msgId) {
|
|||
string1: currentDmId,
|
||||
string2: msgId
|
||||
});
|
||||
await fetchEncrypted("dm/message/pin/toggle", payload);
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/message/pin/toggle" : "dm/message/pin/toggle";
|
||||
await fetchEncrypted(endpoint, payload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showBlahNotification("error.network.error");
|
||||
|
|
@ -3270,7 +3793,8 @@ async function deleteMessage(msgId) {
|
|||
string1: currentDmId,
|
||||
string2: msgId
|
||||
};
|
||||
let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload));
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/message/delete" : "dm/message/delete";
|
||||
let res = await fetchEncrypted(endpoint, JSON.stringify(msgPayload));
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
}
|
||||
|
|
@ -3346,8 +3870,15 @@ async function replyMessage(msgId) {
|
|||
let content = msg ? msg.content : "";
|
||||
if (msg && msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, msg.key);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
if (keyToUse) {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
|
@ -3454,8 +3985,15 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
let rxVal = rx.r;
|
||||
if (currentMsg.key && currentMsg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, currentMsg.key);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
if (keyToUse) {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (rx.u === id && rxVal === reaction) {
|
||||
|
|
@ -3468,10 +4006,17 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
}
|
||||
|
||||
let isAdding = (myReactionId === null);
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let keyToUse = currentDmKey;
|
||||
if (window.currentRoomType === 'groups') {
|
||||
let k = await getGroupKey(currentDmId, currentMsg.key);
|
||||
if(k) keyToUse = k;
|
||||
}
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse));
|
||||
let encryptedReaction = await encryptAesGcmToBase64(reaction, dmKeyBytes);
|
||||
|
||||
let endpoint = isAdding ? "dm/message/react/add" : "dm/message/react/remove";
|
||||
let endpoint = window.currentRoomType === 'groups'
|
||||
? (isAdding ? "group/message/react/add" : "group/message/react/remove")
|
||||
: (isAdding ? "dm/message/react/add" : "dm/message/react/remove");
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId,
|
||||
|
|
@ -3615,7 +4160,7 @@ async function changePassword() {
|
|||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
if (!confirm(processBlah("desc.delete.account") + "\n\nAre you sure?")) return;
|
||||
if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return;
|
||||
showAction("action.account.deleting", "accountdel");
|
||||
let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, "");
|
||||
clearAction("accountdel");
|
||||
|
|
@ -3978,19 +4523,52 @@ if (container) {
|
|||
document.addEventListener('touchend', endDrag);
|
||||
}
|
||||
|
||||
function updateDmListUI(dmId, isRead, timestamp = null) {
|
||||
function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
|
||||
try {
|
||||
if (!window.cachedDms) return;
|
||||
let parsedDms = JSON.parse(window.cachedDms);
|
||||
if (!parsedDms.dms || !parsedDms.dms[dmId]) return;
|
||||
if (type === 'groups') {
|
||||
if (!window.cachedGroups) return;
|
||||
let parsedGroups = JSON.parse(window.cachedGroups);
|
||||
if (!parsedGroups.groups || !parsedGroups.groups[roomId]) return;
|
||||
|
||||
let changed = false;
|
||||
if (parsedDms.dms[dmId].string3 !== (isRead ? "true" : "false")) {
|
||||
parsedDms.dms[dmId].string3 = isRead ? "true" : "false";
|
||||
if (parsedGroups.groups[roomId].string3 !== (isRead ? "true" : "false")) {
|
||||
parsedGroups.groups[roomId].string3 = isRead ? "true" : "false";
|
||||
changed = true;
|
||||
}
|
||||
if (timestamp && parsedDms.dms[dmId].string2 !== timestamp) {
|
||||
parsedDms.dms[dmId].string2 = timestamp;
|
||||
if (timestamp && parsedGroups.groups[roomId].string2 !== timestamp) {
|
||||
parsedGroups.groups[roomId].string2 = timestamp;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
window.cachedGroups = JSON.stringify(parsedGroups);
|
||||
}
|
||||
|
||||
let btn = document.getElementById(`group-btn-${roomId}`);
|
||||
let container = document.getElementById("groups-list");
|
||||
if (btn && container) {
|
||||
let nameSpan = btn.querySelector('.room-name');
|
||||
if (nameSpan) {
|
||||
nameSpan.style.opacity = isRead ? "0.8" : "1";
|
||||
}
|
||||
if (timestamp) {
|
||||
container.prepend(btn);
|
||||
}
|
||||
} else {
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
}
|
||||
} else {
|
||||
if (!window.cachedDms) return;
|
||||
let parsedDms = JSON.parse(window.cachedDms);
|
||||
if (!parsedDms.dms || !parsedDms.dms[roomId]) return;
|
||||
|
||||
let changed = false;
|
||||
if (parsedDms.dms[roomId].string3 !== (isRead ? "true" : "false")) {
|
||||
parsedDms.dms[roomId].string3 = isRead ? "true" : "false";
|
||||
changed = true;
|
||||
}
|
||||
if (timestamp && parsedDms.dms[roomId].string2 !== timestamp) {
|
||||
parsedDms.dms[roomId].string2 = timestamp;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -3998,7 +4576,7 @@ function updateDmListUI(dmId, isRead, timestamp = null) {
|
|||
window.cachedDms = JSON.stringify(parsedDms);
|
||||
}
|
||||
|
||||
let btn = document.getElementById(`dm-btn-${dmId}`);
|
||||
let btn = document.getElementById(`dm-btn-${roomId}`);
|
||||
let container = document.getElementById("dms-list");
|
||||
if (btn && container) {
|
||||
let nameSpan = btn.querySelector('.room-name');
|
||||
|
|
@ -4011,15 +4589,21 @@ function updateDmListUI(dmId, isRead, timestamp = null) {
|
|||
} else {
|
||||
if (typeof refreshDms === 'function') refreshDms(0, false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (typeof refreshDms === 'function') refreshDms(0, false);
|
||||
if (typeof refreshGroups === 'function') refreshGroups(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
function markDmAsRead(dmId) {
|
||||
fetchEncrypted("dm/read", JSON.stringify({string1: dmId})).catch(()=>{});
|
||||
updateDmListUI(dmId, true);
|
||||
function markDmAsRead(roomId, type = 'dms') {
|
||||
if (type === 'groups') {
|
||||
fetchEncrypted("group/read", JSON.stringify({string1: roomId})).catch(()=>{});
|
||||
} else {
|
||||
fetchEncrypted("dm/read", JSON.stringify({string1: roomId})).catch(()=>{});
|
||||
}
|
||||
updateDmListUI(roomId, true, type);
|
||||
}
|
||||
|
||||
function handleDmInteraction() {
|
||||
|
|
@ -4029,10 +4613,19 @@ function handleDmInteraction() {
|
|||
|
||||
if (lastChatScroll <= 10) {
|
||||
try {
|
||||
if (window.currentRoomType === 'groups') {
|
||||
if (window.cachedGroups) {
|
||||
let parsedGroups = JSON.parse(window.cachedGroups);
|
||||
if (parsedGroups.groups && parsedGroups.groups[currentDmId] && parsedGroups.groups[currentDmId].string3 !== "true") {
|
||||
markDmAsRead(currentDmId, window.currentRoomType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (window.cachedDms) {
|
||||
let parsedDms = JSON.parse(window.cachedDms);
|
||||
if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") {
|
||||
markDmAsRead(currentDmId);
|
||||
markDmAsRead(currentDmId, window.currentRoomType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
@ -4056,7 +4649,8 @@ async function showPinnedMessages(forceReload = false, showPanel = true) {
|
|||
}
|
||||
|
||||
try {
|
||||
let res = await fetchEncrypted("dm/messages/pinned", currentDmId);
|
||||
let endpoint = window.currentRoomType === 'groups' ? "group/messages/pinned" : "dm/messages/pinned";
|
||||
let res = await fetchEncrypted(endpoint, currentDmId);
|
||||
if (res && !res.startsWith("error:")) {
|
||||
let msgs = JSON.parse(res);
|
||||
let entries = Object.entries(msgs).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
||||
|
|
|
|||
|
|
@ -117,10 +117,10 @@ var notifisScreen = `
|
|||
var invitesEntry = `
|
||||
<div class="invite-entry">
|
||||
<div class="invite-info">
|
||||
<img src="{pfp}" class="invite-pfp" onclick="openProfile('{id}')" style="cursor: pointer;">
|
||||
<img src="{pfp}" class="invite-pfp" {onclick_attr}>
|
||||
<div class="invite-details">
|
||||
<span class="invite-name" onclick="openProfile('{id}')" style="cursor: pointer;">{username}</span>
|
||||
<span class="invite-desc"><blah>{desc}</blah></span>
|
||||
<span class="invite-name" {onclick_attr}>{username}</span>
|
||||
<span class="invite-desc">{desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invite-actions">
|
||||
|
|
|
|||
|
|
@ -215,6 +215,25 @@ sidebar {
|
|||
width: calc(var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width));
|
||||
border-right: var(--border-width) solid var(--light-border-color);
|
||||
}
|
||||
|
||||
sidebar:not(.second) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
sidebar:not(.second)::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
sidebar:not(.second) {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#roomsbar roomcontent2 {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
sidebar.second {
|
||||
width: 17rem;
|
||||
min-width: 17rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue