Compare commits

..

No commits in common. "55760ba3686c9ef848546b6c5550bae5dad3bea1" and "113530acb8480137d1a66ba1b123f37a351da219" have entirely different histories.

16 changed files with 192 additions and 858 deletions

View file

@ -262,10 +262,5 @@
"action.leaving.group": "Leaving group, meow...", "action.leaving.group": "Leaving group, meow...",
"action.key.rotating": "Rotating keys, meow...", "action.key.rotating": "Rotating keys, meow...",
"action.key.rotation": "Key rotation, meow", "action.key.rotation": "Key rotation, meow",
"desc.leave.group.confirm": "Are you sure you want to leave this group, meow?", "desc.leave.group.confirm": "Are you sure you want to leave this group, meow?"
"desc.key.rotation.you": "You will generate a new encryption key for this group, meow.",
"label.keys.to.encrypt": "keys to encrypt, meow",
"action.group.creating": "Creating group, meow...",
"title.group.settings": "Group settings, meow",
"title.leave.group": "Leave group, meow"
} }

View file

@ -249,13 +249,6 @@
"group.user.joined": "Joined group", "group.user.joined": "Joined group",
"prompt.invite.username": "Enter username to invite:", "prompt.invite.username": "Enter username to invite:",
"desc.failed.load.members": "Failed to load members", "desc.failed.load.members": "Failed to load members",
"title.group.settings": "Group Settings",
"title.group.name": "Group Name",
"title.group.history.visible": "History visible to newcomers",
"desc.group.history.visible": "New group members will be able to see older messages (disables key rotation)",
"history.visible": "enabled",
"history.hidden": "disabled",
"group.history.changed.by": "{0} changed history visibility to: {1}",
"prompt.are.you.sure": "Are you sure?", "prompt.are.you.sure": "Are you sure?",
"title.unknown.group": "Unknown Group", "title.unknown.group": "Unknown Group",
"title.unknown": "Unknown", "title.unknown": "Unknown",
@ -269,10 +262,5 @@
"action.leaving.group": "Leaving group...", "action.leaving.group": "Leaving group...",
"action.key.rotating": "Rotating keys...", "action.key.rotating": "Rotating keys...",
"action.key.rotation": "Key rotation", "action.key.rotation": "Key rotation",
"desc.leave.group.confirm": "Are you sure you want to leave this group?", "desc.leave.group.confirm": "Are you sure you want to leave this group?"
"desc.key.rotation.you": "You will generate a new encryption key for this group.",
"label.keys.to.encrypt": "keys to encrypt",
"action.group.creating": "Creating group...",
"title.group.settings": "Group settings",
"title.leave.group": "Leave group"
} }

View file

@ -248,13 +248,6 @@
"group.user.joined": "Dołączono do grupy", "group.user.joined": "Dołączono do grupy",
"prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:", "prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:",
"desc.failed.load.members": "Nie udało się załadować członków", "desc.failed.load.members": "Nie udało się załadować członków",
"title.group.settings": "Ustawienia grupy",
"title.group.name": "Nazwa grupy",
"title.group.history.visible": "Historia widoczna dla nowych",
"desc.group.history.visible": "Nowi członkowie grupy będą mogli zobaczyć starsze wiadomości (wyłącza rotację kluczy)",
"history.visible": "włączona",
"history.hidden": "wyłączona",
"group.history.changed.by": "{0} zmienił widoczność historii na: {1}",
"prompt.are.you.sure": "Czy na pewno?", "prompt.are.you.sure": "Czy na pewno?",
"title.unknown.group": "Nieznana grupa", "title.unknown.group": "Nieznana grupa",
"title.unknown": "Nieznany", "title.unknown": "Nieznany",
@ -269,10 +262,5 @@
"action.leaving.group": "Opuszczanie grupy...", "action.leaving.group": "Opuszczanie grupy...",
"action.key.rotating": "Obracanie kluczy...", "action.key.rotating": "Obracanie kluczy...",
"action.key.rotation": "Rotacja kluczy", "action.key.rotation": "Rotacja kluczy",
"desc.leave.group.confirm": "Czy na pewno chcesz opuścić tę grupę?", "desc.leave.group.confirm": "Czy na pewno chcesz opuścić tę grupę?"
"desc.key.rotation.you": "Wygenerujesz nowy klucz szyfrujący dla tej grupy.",
"label.keys.to.encrypt": "kluczy do zaszyfrowania",
"action.group.creating": "Tworzenie grupy...",
"title.group.settings": "Ustawienia grupy",
"title.leave.group": "Opuść grupę"
} }

View file

@ -30,10 +30,11 @@ const ICONS = {
pin: (props) => ICONS._material(`<path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"/>`, props), pin: (props) => ICONS._material(`<path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"/>`, props),
call: (props) => ICONS._material(`<path d="M798-120q-125 0-247-54.5T329-329Q229-429 174.5-551T120-798q0-18 12-30t30-12h162q14 0 25 9.5t13 22.5l26 140q2 16-1 27t-11 19l-97 98q20 37 47.5 71.5T387-386q31 31 65 57.5t72 48.5l94-94q9-9 23.5-13.5T670-390l138 28q14 4 23 14.5t9 23.5v162q0 18-12 30t-30 12ZM241-600l66-66-17-94h-89q5 41 14 81t26 79Zm358 358q39 17 79.5 27t81.5 13v-88l-94-19-67 67ZM241-600Zm358 358Z"/>`, props), call: (props) => ICONS._material(`<path d="M798-120q-125 0-247-54.5T329-329Q229-429 174.5-551T120-798q0-18 12-30t30-12h162q14 0 25 9.5t13 22.5l26 140q2 16-1 27t-11 19l-97 98q20 37 47.5 71.5T387-386q31 31 65 57.5t72 48.5l94-94q9-9 23.5-13.5T670-390l138 28q14 4 23 14.5t9 23.5v162q0 18-12 30t-30 12ZM241-600l66-66-17-94h-89q5 41 14 81t26 79Zm358 358q39 17 79.5 27t81.5 13v-88l-94-19-67 67ZM241-600Zm358 358Z"/>`, props),
account: (props) => ICONS._material(`<path d="M200-200h560v-560H200v560Zm0 80q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-160h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/>`, props), account: (props) => ICONS._material(`<path d="M200-200h560v-560H200v560Zm0 80q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-160h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/>`, props),
settings: (props) => ICONS._material(`<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 167-116.5 283.5T480-80Zm0-157q102 0 174-72t72-174q0-102-72-174t-174-72q-102 0-174 72t-72 174q0 102 72 174t174 72Zm0-80q-68 0-116.5-48.5T315-482q0-68 48.5-116.5T480-647q68 0 116.5 48.5T645-482q0 68-48.5 116.5T480-317Zm0-60q43 0 74-31t31-74q0-43-31-74t-74-31q-43 0-74 31t-31 74q0 43 31 74t74 31Zm0-105Z"/>`, props),
profile: (props) => ICONS._material(`<path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/>`, props), profile: (props) => ICONS._material(`<path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/>`, props),
replyAction: (props) => ICONS._material(`<path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/>`, props), replyAction: (props) => ICONS._material(`<path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/>`, props),
react: (props) => ICONS._material(`<path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>`, props), react: (props) => ICONS._material(`<path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>`, props),
settings: (props) => ICONS._material(`<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/>`, props), settingsProfile: (props) => ICONS._material(`<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/>`, props),
download: (props) => ICONS._feather(`<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>`, props), download: (props) => ICONS._feather(`<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>`, props),
camera: (props) => ICONS._feather(`<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle>`, props), camera: (props) => ICONS._feather(`<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle>`, props),

View file

@ -256,12 +256,9 @@
const roomKey = randomRoomKey256Chars(); const roomKey = randomRoomKey256Chars();
const selfWrapped = await wrapRoomKeyForSelf(roomKey); const selfWrapped = await wrapRoomKeyForSelf(roomKey);
let historyVisible = document.getElementById("creategroup-history") ? document.getElementById("creategroup-history").checked.toString() : "false";
let payload = JSON.stringify({ let payload = JSON.stringify({
string1: groupName, string1: groupName,
string2: selfWrapped, string2: selfWrapped
string3: historyVisible
}); });
let res = await fetchEncrypted("group/create", payload); let res = await fetchEncrypted("group/create", payload);

View file

@ -432,7 +432,7 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey,
let popupContainer = document.getElementById("generic-popup-container"); let popupContainer = document.getElementById("generic-popup-container");
let popupInner = document.getElementById("generic-popup-inner"); let popupInner = document.getElementById("generic-popup-inner");
let confirmClass = isDestructive ? "popup-btn-danger" : ""; let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm";
genericPopupCancelCallback = onCancel; genericPopupCancelCallback = onCancel;
@ -449,10 +449,10 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey,
${descHtml} ${descHtml}
</div> </div>
<div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;"> <div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;">
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()"> <button class="profile-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
<blah>${cancelActionKey}</blah> <blah>${cancelActionKey}</blah>
</button> </button>
<button class="profile-action-btn popup-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()"> <button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
<blah>${confirmActionKey}</blah> <blah>${confirmActionKey}</blah>
</button> </button>
</div> </div>
@ -474,7 +474,7 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c
let popupContainer = document.getElementById("generic-popup-container"); let popupContainer = document.getElementById("generic-popup-container");
let popupInner = document.getElementById("generic-popup-inner"); let popupInner = document.getElementById("generic-popup-inner");
let confirmClass = isDestructive ? "popup-btn-danger" : ""; let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm";
genericPopupCancelCallback = onCancel; genericPopupCancelCallback = onCancel;
@ -491,15 +491,12 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c
<div style="width: 100%; text-align: center; color: var(--text-color); opacity: 0.8; margin-top: 0.5rem; margin-bottom: 1rem;"> <div style="width: 100%; text-align: center; color: var(--text-color); opacity: 0.8; margin-top: 0.5rem; margin-bottom: 1rem;">
${descHtml} ${descHtml}
</div> </div>
<div class="input-group" style="margin: 0 auto 1.5rem auto; width: 100%;"> <input type="text" id="generic-popup-input" class="forminput" placeholder="{blah(${placeholderKey})}" style="width: 100%; margin-bottom: 1rem; box-sizing: border-box;" onkeydown="if(arguments[0].key === 'Enter') { if(genericPopupConfirmCallback) genericPopupConfirmCallback(); }">
<label for="generic-popup-input" style="margin-left: calc((var(--button-margin) * 2) - 0.6rem);"><blah>${placeholderKey}</blah></label>
<input type="text" id="generic-popup-input" class="forminput" placeholder="${processBlah(placeholderKey)}" style="margin-left: 0; margin-right: 0; width: 100%; box-sizing: border-box;" onkeydown="if(arguments[0].key === 'Enter') { if(genericPopupConfirmCallback) genericPopupConfirmCallback(); }">
</div>
<div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;"> <div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;">
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()"> <button class="profile-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
<blah>${cancelActionKey}</blah> <blah>${cancelActionKey}</blah>
</button> </button>
<button class="profile-action-btn popup-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()"> <button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
<blah>${confirmActionKey}</blah> <blah>${confirmActionKey}</blah>
</button> </button>
</div> </div>
@ -534,7 +531,7 @@ function closeRotationPopup() {
async function showRotationPopup(groupId, originalMessagePayload) { async function showRotationPopup(groupId, originalMessagePayload) {
pendingRotationMessagePayload = originalMessagePayload; pendingRotationMessagePayload = originalMessagePayload;
showAction("action.key.rotating", "key_rotation"); showAction("action.key.rotation", "key_rotation");
try { try {
let membersRaw = await fetchEncrypted("group/members", groupId); let membersRaw = await fetchEncrypted("group/members", groupId);
@ -542,7 +539,7 @@ async function showRotationPopup(groupId, originalMessagePayload) {
let members = JSON.parse(membersRaw); let members = JSON.parse(membersRaw);
let descHtml = `<blah>desc.key.rotation.you</blah><br><br><b>${members.length} <blah>label.keys.to.encrypt</blah></b>`; let descHtml = `<blah>desc.key.rotation.info</blah><br><br><b>${members.length} <blah>label.members</blah></b>`;
showConfirmPopup( showConfirmPopup(
"title.key.rotation", "title.key.rotation",
@ -560,7 +557,6 @@ async function showRotationPopup(groupId, originalMessagePayload) {
async function executeKeyRotation() { async function executeKeyRotation() {
closeRotationPopup(); closeRotationPopup();
clearAction("key_rotation");
showAction("action.key.rotating", "key_rotation_exec"); showAction("action.key.rotating", "key_rotation_exec");
try { try {
@ -617,12 +613,29 @@ async function executeKeyRotation() {
clearAction("key_rotation_exec"); clearAction("key_rotation_exec");
showBlahNotification("success:key.rotated"); 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; pendingRotationMessagePayload = null;
} catch(e) { } catch(e) {
clearAction("key_rotation_exec"); clearAction("key_rotation_exec");
showBlahNotification(e.message || e); console.error(e);
pendingRotationMessagePayload = null; showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong");
} }
} }
@ -2128,7 +2141,7 @@ async function renderInvites(res, type) {
let actions = ""; let actions = "";
let desc = ""; let desc = "";
let displayName = username || onlyId; let displayName = username.split(':')[0] || onlyId;
if (type === "received") { if (type === "received") {
desc = `:desc.invite.dm.received:${displayName};${onlyId}`; desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
actions = ` actions = `
@ -2182,7 +2195,7 @@ async function renderInvites(res, type) {
let actions = ""; let actions = "";
let desc = ""; let desc = "";
let displayName = username || onlyId; let displayName = username.split(':')[0] || onlyId;
let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`; let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`;
let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`; let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`;
@ -2386,13 +2399,6 @@ function gotoJoinSpace() {
} }
async function gotoHome(splash = true) { async function gotoHome(splash = true) {
if (dmMessagePollInterval) {
clearInterval(dmMessagePollInterval);
dmMessagePollInterval = null;
}
currentDmId = null;
window.currentRoomType = null;
if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar
setActiveSidebarIndicator(sidebarHomeIndicator); setActiveSidebarIndicator(sidebarHomeIndicator);
if (splash) { if (splash) {
@ -2585,8 +2591,6 @@ async function renderDms(res) {
let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27"); let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27");
let unreadBadge = `<div class="unread-badge ${isRead ? "" : "show"}"></div>`;
html += ` html += `
<button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', decodeURIComponent('${safeTargetUsername}'), '${targetId}')"> <button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', decodeURIComponent('${safeTargetUsername}'), '${targetId}')">
<img src="${pfp}" class="room-pfp"> <img src="${pfp}" class="room-pfp">
@ -2594,7 +2598,6 @@ async function renderDms(res) {
<span class="room-name" style="${nameStyle}">${displayName}</span> <span class="room-name" style="${nameStyle}">${displayName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername} (${targetId.split(';')[0]})</span> <span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername} (${targetId.split(';')[0]})</span>
</div> </div>
${unreadBadge}
</button> </button>
`; `;
count++; count++;
@ -2648,8 +2651,6 @@ async function renderGroups(res) {
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27"); let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
let pfp = await getGroupAvatarUrl(groupId, groupName); let pfp = await getGroupAvatarUrl(groupId, groupName);
let unreadBadge = `<div class="unread-badge ${isRead ? "" : "show"}"></div>`;
html += ` html += `
<button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')" oncontextmenu="handleGroupContextMenu(event, '${groupId}')"> <button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')" oncontextmenu="handleGroupContextMenu(event, '${groupId}')">
<img src="${pfp}" class="room-pfp"> <img src="${pfp}" class="room-pfp">
@ -2657,7 +2658,6 @@ async function renderGroups(res) {
<span class="room-name" style="${nameStyle}">${groupName}</span> <span class="room-name" style="${nameStyle}">${groupName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span> <span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span>
</div> </div>
${unreadBadge}
</button> </button>
`; `;
count++; count++;
@ -2742,7 +2742,7 @@ async function inviteToGroupPrompt(groupId) {
showPromptPopup( showPromptPopup(
"action.invite", "action.invite",
"", "",
"title.username", "prompt.invite.username",
"action.invite", "action.invite",
"action.cancel", "action.cancel",
async (targetUsername) => { async (targetUsername) => {
@ -2762,63 +2762,9 @@ async function inviteToGroupPrompt(groupId) {
try { try {
showAction("action.invite.sending", "invite.action"); showAction("action.invite.sending", "invite.action");
let keysPayload = "";
try {
let infoRes = await fetchEncrypted("group/info", groupId);
if (infoRes && !infoRes.startsWith("error:")) {
let info = JSON.parse(infoRes);
if (info.string6 === "true") { // historyVisible
let lastKeyIndex = parseInt(info.string4);
if (!isNaN(lastKeyIndex)) {
let publicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${targetId}`);
if (publicKeysRaw && publicKeysRaw.length > 10) {
let publicKeys = JSON.parse(publicKeysRaw);
let hybrid = await deriveHybridKeyToUser(publicKeys);
let myPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${window.id}`);
let keysDict = {};
let allKeysSuccess = true;
for (let i = 0; i <= lastKeyIndex; i++) {
let roomKey = await getGroupKey(groupId, i.toString());
if (!roomKey) {
allKeysSuccess = false;
break;
}
let encForMember = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes);
let setupPayloadJson = JSON.stringify({
v: 1,
alg: "hybrid-x25519-mlkem768",
ctMlKem: hybrid.ciphertextMlKemBase64,
enc: encForMember
});
keysDict[i.toString()] = `SETUP:${myPublicKeysRaw};${setupPayloadJson}`;
}
if (!allKeysSuccess) {
clearAction("invite.action");
showBlahNotification("error:failed.to.prepare.keys");
return;
}
keysPayload = JSON.stringify(keysDict);
} else {
clearAction("invite.action");
showBlahNotification("error:failed.to.prepare.keys");
return;
}
}
}
}
} catch (e) {
console.error("Error generating keys payload for invite", e);
clearAction("invite.action");
showBlahNotification("error:failed.to.prepare.keys");
return;
}
let payload = JSON.stringify({ let payload = JSON.stringify({
string1: groupId, string1: groupId,
string2: targetId, string2: targetId
string3: keysPayload
}); });
let res = await fetchEncrypted("group/invite", payload); let res = await fetchEncrypted("group/invite", payload);
@ -2911,10 +2857,10 @@ async function openDm(dmId, username, targetId, type = 'dms') {
let iconHtml = ""; let iconHtml = "";
if (type === 'dms') { if (type === 'dms') {
let pfp = await getAvatarUrl(targetId, username); let pfp = await getAvatarUrl(targetId, username);
iconHtml = `<img src="${pfp}" style="width: 2.3rem;height: 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 { } else {
let pfp = await getGroupAvatarUrl(dmId, username); let pfp = await getGroupAvatarUrl(dmId, username);
iconHtml = `<img src="${pfp}" style="width: 2.3rem;height: 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);">`;
} }
if (currentRoomsBarTitle !== "title.home") { if (currentRoomsBarTitle !== "title.home") {
@ -3072,27 +3018,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
if (atts.length > 0) { if (atts.length > 0) {
attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">`; attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">`;
for (let att of atts) { for (let att of atts) {
if (att.enc && att.enc.startsWith("ENC:")) {
try {
if (keyToUse) {
let keyBytes = await sha256Bytes(base64ToUint8(keyToUse));
let decStr = await decryptAesGcmFromBase64(att.enc.substring(4), keyBytes);
let metadata = JSON.parse(decStr);
att.name = metadata.name || "unknown";
att.type = metadata.type || "application/octet-stream";
att.size = metadata.size || 0;
} else {
att.name = processBlah("error:messages.decrypt.failed");
att.type = "application/octet-stream";
}
} catch(e) {
att.name = processBlah("error:messages.decrypt.failed");
att.type = "application/octet-stream";
}
}
let attContainerId = `att-container-${msgId}-${att.id}`; let attContainerId = `att-container-${msgId}-${att.id}`;
let isMedia = att.type && (att.type.startsWith("image/") || att.type.startsWith("video/")); let isMedia = att.type.startsWith("image/") || att.type.startsWith("video/");
let sizeStr = att.size ? formatBytes(att.size) : ""; let sizeStr = att.size ? formatBytes(att.size) : "";
let displayName = truncateFilename(att.name, 25); let displayName = truncateFilename(att.name, 25);
let iconSvg = isMedia let iconSvg = isMedia
@ -3133,7 +3060,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
content = processBlah(content); content = processBlah(content);
if (msg.type && msg.type.startsWith("larp.info")) { if (msg.type && msg.type.startsWith("larp.info")) {
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" || msg.type === "larp.info.history") { 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"); let authorName = processBlah("title.unknown");
if (msg.author && msg.author !== "0") { if (msg.author && msg.author !== "0") {
if (!dmUsernameCache[msg.author]) { if (!dmUsernameCache[msg.author]) {
@ -3156,7 +3083,6 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
else if (msg.type === "larp.info.join") actionStr = "group.user.joined.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.leave") actionStr = "group.user.left.by";
else if (msg.type === "larp.info.create") actionStr = "group.created.by"; else if (msg.type === "larp.info.create") actionStr = "group.created.by";
else if (msg.type === "larp.info.history") actionStr = "group.history.changed.by";
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`; let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
@ -3164,8 +3090,6 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) { if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) {
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`; let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${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); content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg);
} else if (msg.type === "larp.info.history") {
content = textStr.replace("{0}", clickableAuthor).replace("{1}", content);
} else { } else {
content = textStr.replace("{0}", clickableAuthor); content = textStr.replace("{0}", clickableAuthor);
} }
@ -3597,10 +3521,7 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name,
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (window.currentRoomType === 'groups' && window.groupKeysCache[currentDmId]) { container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading</div>`;
delete window.groupKeysCache[currentDmId][keyIndex];
}
container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading - click to retry</div>`;
container.dataset.loading = "false"; container.dataset.loading = "false";
} }
} }
@ -3727,17 +3648,12 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
} }
if (decText && decText.startsWith("success:")) { if (decText && decText.startsWith("success:")) {
let metadataObj = {
type: file.type,
name: file.name,
size: file.size
};
let encMetadata = "ENC:" + await encryptAesGcmToBase64(JSON.stringify(metadataObj), dmKeyBytes);
attachmentsPayload.push({ attachmentsPayload.push({
id: i, id: i,
staging_id: decText.split(":")[1], type: file.type,
enc: encMetadata name: file.name,
size: file.size,
staging_id: decText.split(":")[1]
}); });
} else { } else {
throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed"); throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed");
@ -3755,11 +3671,9 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
if (window.currentRoomType === 'groups') { if (window.currentRoomType === 'groups') {
msgPayload.string5 = window.currentGroupKeyIndex || "0"; msgPayload.string5 = window.currentGroupKeyIndex || "0";
msgPayload.rawPlainContent = content; // for retry
} }
let savedContentForRestore = content;
let savedAttachmentsForRestore = [...currentDraftAttachments];
input.value = ""; input.value = "";
input.style.height = '2.5rem'; input.style.height = '2.5rem';
sendbutton.style.height = '2.5rem'; sendbutton.style.height = '2.5rem';
@ -3779,13 +3693,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') { if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') {
let pElem = document.getElementById(pendingMsgId); let pElem = document.getElementById(pendingMsgId);
if (pElem) pElem.remove(); if (pElem) pElem.remove();
showRotationPopup(currentDmId, msgPayload);
// Restore input and attachments for the user to retry manually
input.value = savedContentForRestore;
currentDraftAttachments = savedAttachmentsForRestore;
renderAttachmentsPreview();
showRotationPopup(currentDmId, null);
} else if (res && res.startsWith("error:")) { } else if (res && res.startsWith("error:")) {
showBlahNotification(res); showBlahNotification(res);
let pElem = document.getElementById(pendingMsgId); let pElem = document.getElementById(pendingMsgId);
@ -4374,12 +4282,7 @@ async function changePassword() {
} }
async function deleteAccount() { async function deleteAccount() {
showConfirmPopup( if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return;
"title.delete.account",
"<blah>desc.delete.account</blah><br><br><blah>prompt.are.you.sure</blah>",
"action.delete.account",
"action.cancel",
async () => {
showAction("action.account.deleting", "accountdel"); showAction("action.account.deleting", "accountdel");
let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, "");
clearAction("accountdel"); clearAction("accountdel");
@ -4390,10 +4293,6 @@ async function deleteAccount() {
} else { } else {
showBlahNotification(text); showBlahNotification(text);
} }
},
null,
true
);
} }
async function saveProfile() { async function saveProfile() {
@ -4450,8 +4349,8 @@ function initCropper() {
let zoomSlider = document.getElementById('cropper-zoom'); let zoomSlider = document.getElementById('cropper-zoom');
// Set aspect ratio // Set aspect ratio
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494; let targetWidth = cropperType === 'avatar' ? 512 : 494;
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200; let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
let aspectRatio = targetWidth / targetHeight; let aspectRatio = targetWidth / targetHeight;
container.style.height = 'auto'; container.style.height = 'auto';
@ -4519,8 +4418,8 @@ async function confirmCropper() {
let zoom = parseFloat(document.getElementById('cropper-zoom').value); let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let container = document.getElementById('cropper-viewport-container'); let container = document.getElementById('cropper-viewport-container');
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988; let targetWidth = cropperType === 'avatar' ? 512 : 988;
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 400; let targetHeight = cropperType === 'avatar' ? 512 : 400;
let scaleRatio = targetWidth / container.clientWidth; let scaleRatio = targetWidth / container.clientWidth;
let drawX = cropperPosX * scaleRatio; let drawX = cropperPosX * scaleRatio;
let drawY = cropperPosY * scaleRatio; let drawY = cropperPosY * scaleRatio;
@ -4691,20 +4590,10 @@ async function confirmCropper() {
newSrc = 'data:image/webp;base64,' + b64; newSrc = 'data:image/webp;base64,' + b64;
} }
let payloadKey = (cropperType === 'avatar' || cropperType === 'group-avatar') ? 'larp.profile.pfp' : 'larp.profile.banner'; let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
showAction("action.uploading", "uploadmedia");
let res;
if (cropperType === 'group-avatar') {
let payload = JSON.stringify({
string1: window.currentSettingsGroupId,
string2: payloadKey,
string3: b64
});
res = await fetchEncrypted("group/storage/public/update", payload);
} else {
let fileBytes = base64ToUint8(b64); let fileBytes = base64ToUint8(b64);
res = await fetchEncryptedUpload(payloadKey, fileBytes); showAction("action.uploading", "uploadmedia");
} let res = await fetchEncryptedUpload(payloadKey, fileBytes);
clearAction("uploadmedia"); clearAction("uploadmedia");
if (res && res.startsWith("error:")) { if (res && res.startsWith("error:")) {
showBlahNotification(res); showBlahNotification(res);
@ -4714,15 +4603,9 @@ async function confirmCropper() {
// Update all matching images on the page instantly // Update all matching images on the page instantly
document.querySelectorAll('img').forEach(img => { document.querySelectorAll('img').forEach(img => {
if (cropperType === 'group-avatar') {
if (img.src.includes(`id=${window.currentSettingsGroupId}&e=${payloadKey}`) || img.id === 'group-settings-avatar-preview') {
img.src = newSrc;
}
} else {
if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) { if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) {
img.src = newSrc; img.src = newSrc;
} }
}
}); });
} }
@ -4790,11 +4673,6 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
if (nameSpan) { if (nameSpan) {
nameSpan.style.opacity = isRead ? "0.8" : "1"; nameSpan.style.opacity = isRead ? "0.8" : "1";
} }
let badge = btn.querySelector('.unread-badge');
if (badge) {
if (isRead) badge.classList.remove("show");
else badge.classList.add("show");
}
if (timestamp) { if (timestamp) {
container.prepend(btn); container.prepend(btn);
} }
@ -4827,11 +4705,6 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
if (nameSpan) { if (nameSpan) {
nameSpan.style.opacity = isRead ? "0.8" : "1"; nameSpan.style.opacity = isRead ? "0.8" : "1";
} }
let badge = btn.querySelector('.unread-badge');
if (badge) {
if (isRead) badge.classList.remove("show");
else badge.classList.add("show");
}
if (timestamp) { if (timestamp) {
container.prepend(btn); container.prepend(btn);
} }
@ -5054,48 +4927,9 @@ function handleGroupContextMenu(e, groupId) {
fixedContextMenu.style.top = `${newTop}px`; fixedContextMenu.style.top = `${newTop}px`;
} }
async function gotoGroupSettings(groupId) { function gotoGroupSettings(groupId) {
if (fixedContextMenu) fixedContextMenu.classList.remove("show"); if (fixedContextMenu) fixedContextMenu.classList.remove("show");
// Not implemented yet
window.currentSettingsGroupId = groupId;
let infoStr = await fetchEncrypted("group/info", groupId);
if (!infoStr.startsWith("error:")) {
let info = JSON.parse(infoStr);
await switchRoomContent("title.group.settings", groupSettingsScreen, false);
document.getElementById('group-settings-name').value = info.string2;
let pfpUrl = await getGroupAvatarUrl(groupId, info.string2);
document.getElementById('group-settings-avatar-preview').src = pfpUrl;
document.getElementById('group-settings-history').checked = (info.string6 === "true");
} else {
showBlahNotification(infoStr);
}
}
async function saveGroupSettings() {
let groupId = window.currentSettingsGroupId;
let name = document.getElementById('group-settings-name').value;
let historyVisible = document.getElementById('group-settings-history').checked.toString();
let payload = JSON.stringify({
string1: groupId,
string2: name,
string3: historyVisible
});
showAction("action.saving", "savegroup");
let res = await fetchEncrypted("group/settings/update", payload);
clearAction("savegroup");
if (res === "success") {
showBlahNotification("success:group.settings.saved");
refreshGroups(0, false);
} else {
showBlahNotification(res);
}
} }
function leaveGroupPrompt(groupId) { function leaveGroupPrompt(groupId) {

View file

@ -31,18 +31,8 @@ var createGroupScreen = `
<div class="input-group"> <div class="input-group">
<label for="creategroup-name"><blah>title.name</blah></label> <label for="creategroup-name"><blah>title.name</blah></label>
<input type="text" id="creategroup-name" placeholder="{blah(placeholder.create.group.input)}" class="forminput"> <input type="text" id="creategroup-name" placeholder="{blah(placeholder.create.group.input)}" class="forminput">
<div class="setting-row" style="text-align: left;">
<div class="setting-info">
<span class="setting-title"><blah>title.group.history.visible</blah></span>
<span class="setting-desc"><blah>desc.group.history.visible</blah></span>
</div> </div>
<label class="toggle-switch"> <div class="input-group">
<input type="checkbox" id="creategroup-history">
<span class="toggle-slider"></span>
</label>
</div>
<button class="submit-button blah" onclick="createGroup()">title.create.group</button> <button class="submit-button blah" onclick="createGroup()">title.create.group</button>
</div> </div>
</div> </div>
@ -221,7 +211,7 @@ var profileContextMenu = `
<span class="blah">title.profile</span> <span class="blah">title.profile</span>
</button> </button>
<button onclick="gotoSettings()"> <button onclick="gotoSettings()">
${ICONS.settings()} ${ICONS.settingsProfile()}
<span class="blah">title.settings</span> <span class="blah">title.settings</span>
</button> </button>
<button onclick="logout()"> <button onclick="logout()">
@ -337,7 +327,7 @@ var accountSettings = `
<div class="input-group"> <div class="input-group">
<label style="color: var(--big-red);"><blah>title.delete.account</blah></label> <label style="color: var(--big-red);"><blah>title.delete.account</blah></label>
<p style="font-size: 0.85rem; opacity: 0.7; margin-bottom: 0.5rem; margin-left: 0.6rem;"><blah>desc.delete.account</blah></p> <p style="font-size: 0.85rem; opacity: 0.7; margin-bottom: 0.5rem; margin-left: 0.6rem;"><blah>desc.delete.account</blah></p>
<button class="submit-button popup-btn-danger blah" onclick="deleteAccount()">action.delete.account</button> <button class="submit-button blah" onclick="deleteAccount()" style="background: var(--big-red); color: white;">action.delete.account</button>
</div> </div>
</div> </div>
`; `;
@ -378,36 +368,6 @@ var profileSettings = `
</div> </div>
</div> </div>
`; `;
var groupSettingsScreen = `
<div class="auth-box popup-auto-height" style="margin-top:2rem">
<div class="input-group" style="display: flex; flex-direction: row; align-items: stretch; justify-content: space-between; border: var(--border-width) solid var(--light-border-color); border-radius: 0.8rem; padding: 1rem; width: 100%; max-width: 30rem;">
<img id="group-settings-avatar-preview" src="" style="width: 4.5rem; height: 4.5rem; border-radius: 0.8rem; border: var(--border-width) solid rgba(255, 255, 255, 0.2); object-fit: cover; background: var(--main-bg-color);">
<div style="display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; padding: 0.1rem 0;">
<div style="font-weight: bold;"><blah>title.avatar</blah></div>
<button class="submit-button blah" style="margin: 0; padding: 0.5rem 1rem; width: auto;" onclick="document.getElementById('group-settings-avatar-input').click()">action.change.avatar</button>
<input type="file" id="group-settings-avatar-input" style="display: none;" accept="image/*" onchange="openCropper(this, 'group-avatar')">
</div>
</div>
<br>
<div class="input-group">
<label for="group-settings-name"><blah>title.group.name</blah></label>
<input type="text" id="group-settings-name" class="forminput">
<div class="setting-row">
<div class="setting-info">
<span class="setting-title"><blah>title.group.history.visible</blah></span>
<span class="setting-desc"><blah>desc.group.history.visible</blah></span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="group-settings-history">
<span class="toggle-slider"></span>
</label>
</div>
<button class="submit-button blah" onclick="saveGroupSettings()">action.save</button>
</div>
</div>
`;
var appearanceSettings = ` var appearanceSettings = `
<div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;"> <div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;">

View file

@ -366,7 +366,6 @@ herotitle {
gap: 0.2rem; gap: 0.2rem;
width: 90%; width: 90%;
max-width: 30rem; max-width: 30rem;
margin: 0 auto;
} }
.input-group label { .input-group label {
font-size: 0.75rem; font-size: 0.75rem;
@ -879,27 +878,35 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
height: auto; height: auto;
max-height: 85vh; max-height: 85vh;
} }
button.profile-action-btn.popup-action-btn { .popup-btn-cancel {
flex: 1;
display: flex;
justify-content: center;
margin-top: 0;
}
button.profile-action-btn.popup-btn-cancel {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
color: var(--text-color); color: var(--text-color);
border: var(--border-width) solid var(--light-border-color); border: var(--border-width) solid var(--light-border-color);
flex: 1;
justify-content: center;
} }
button.profile-action-btn.popup-btn-cancel:hover { .popup-btn-cancel:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
} }
button.profile-action-btn.popup-btn-danger, button.submit-button.popup-btn-danger { .popup-btn-confirm {
background: var(--big-red) !important; background: var(--text-color);
color: white !important; color: var(--main-bg-color);
border: none; border: none;
flex: 1;
justify-content: center;
} }
button.profile-action-btn.popup-btn-danger:hover, button.submit-button.popup-btn-danger:hover { .popup-btn-confirm:hover {
filter: brightness(0.85); background: white;
}
.popup-btn-danger {
background: var(--big-red);
color: white;
border: none;
flex: 1;
justify-content: center;
}
.popup-btn-danger:hover {
background: #ff5252;
} }
.profile-popup-container.show .profile-popup-content { .profile-popup-container.show .profile-popup-content {
transform: scale(1); transform: scale(1);
@ -1065,106 +1072,3 @@ body.hide-topbar-pin #roomdetailsbar roomtopbar .room-action-pin { display: flex
body.hide-topbar-call roomcontent roomtopbar .room-action-call { display: none !important; } body.hide-topbar-call roomcontent roomtopbar .room-action-call { display: none !important; }
body.hide-topbar-call #roomdetailsbar roomtopbar .room-action-call { display: flex !important; } body.hide-topbar-call #roomdetailsbar roomtopbar .room-action-call { display: flex !important; }
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 3.4rem;
height: 1.8rem;
margin: var(--button-margin);
flex-shrink: 0;
}
label.toggle-switch {
opacity: 0.8;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .4s;
border-radius: 1.8rem;
border: var(--border-width) solid var(--light-border-color);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.2rem;
width: 1.2rem;
left: 0.25rem;
top: 50%;
transform: translateY(-50%);
background-color: var(--text-color);
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--text-color) !important;
border-color: var(--text-color) !important;
}
input:checked + .toggle-slider:before {
transform: translate(1.6rem, -50%);
background-color: var(--main-bg-color) !important;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
width: auto;
box-sizing: border-box;
margin: var(--button-margin);
padding: 0.5rem;
border-radius: 0.8rem;
background-color: rgba(255, 255, 255, 0.02);
}
.setting-row:hover {
background-color: rgba(255, 255, 255, 0.04);
}
.setting-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.setting-title {
font-size: 1.1rem;
font-weight: bold;
}
.setting-desc {
font-size: 0.85rem;
color: var(--darker-text-color);
margin-top: 0.2rem;
}
.unread-badge {
width: 0.65rem;
height: 0.65rem;
min-width: 0.65rem;
min-height: 0.65rem;
background-color: var(--big-red);
border-radius: 50%;
flex-shrink: 0;
margin-right: 0.5rem;
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform: scale(0);
}
.unread-badge.show {
transform: scale(1);
animation: badge-appear 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes badge-appear {
0% { transform: scale(0); }
100% { transform: scale(1); }
}

View file

@ -262,10 +262,5 @@
"action.leaving.group": "Leaving group, meow...", "action.leaving.group": "Leaving group, meow...",
"action.key.rotating": "Rotating keys, meow...", "action.key.rotating": "Rotating keys, meow...",
"action.key.rotation": "Key rotation, meow", "action.key.rotation": "Key rotation, meow",
"desc.leave.group.confirm": "Are you sure you want to leave this group, meow?", "desc.leave.group.confirm": "Are you sure you want to leave this group, meow?"
"desc.key.rotation.you": "You will generate a new encryption key for this group, meow.",
"label.keys.to.encrypt": "keys to encrypt, meow",
"action.group.creating": "Creating group, meow...",
"title.group.settings": "Group settings, meow",
"title.leave.group": "Leave group, meow"
} }

View file

@ -249,13 +249,6 @@
"group.user.joined": "Joined group", "group.user.joined": "Joined group",
"prompt.invite.username": "Enter username to invite:", "prompt.invite.username": "Enter username to invite:",
"desc.failed.load.members": "Failed to load members", "desc.failed.load.members": "Failed to load members",
"title.group.settings": "Group Settings",
"title.group.name": "Group Name",
"title.group.history.visible": "History visible to newcomers",
"desc.group.history.visible": "New group members will be able to see older messages (disables key rotation)",
"history.visible": "enabled",
"history.hidden": "disabled",
"group.history.changed.by": "{0} changed history visibility to: {1}",
"prompt.are.you.sure": "Are you sure?", "prompt.are.you.sure": "Are you sure?",
"title.unknown.group": "Unknown Group", "title.unknown.group": "Unknown Group",
"title.unknown": "Unknown", "title.unknown": "Unknown",
@ -269,10 +262,5 @@
"action.leaving.group": "Leaving group...", "action.leaving.group": "Leaving group...",
"action.key.rotating": "Rotating keys...", "action.key.rotating": "Rotating keys...",
"action.key.rotation": "Key rotation", "action.key.rotation": "Key rotation",
"desc.leave.group.confirm": "Are you sure you want to leave this group?", "desc.leave.group.confirm": "Are you sure you want to leave this group?"
"desc.key.rotation.you": "You will generate a new encryption key for this group.",
"label.keys.to.encrypt": "keys to encrypt",
"action.group.creating": "Creating group...",
"title.group.settings": "Group settings",
"title.leave.group": "Leave group"
} }

View file

@ -248,13 +248,6 @@
"group.user.joined": "Dołączono do grupy", "group.user.joined": "Dołączono do grupy",
"prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:", "prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:",
"desc.failed.load.members": "Nie udało się załadować członków", "desc.failed.load.members": "Nie udało się załadować członków",
"title.group.settings": "Ustawienia grupy",
"title.group.name": "Nazwa grupy",
"title.group.history.visible": "Historia widoczna dla nowych",
"desc.group.history.visible": "Nowi członkowie grupy będą mogli zobaczyć starsze wiadomości (wyłącza rotację kluczy)",
"history.visible": "włączona",
"history.hidden": "wyłączona",
"group.history.changed.by": "{0} zmienił widoczność historii na: {1}",
"prompt.are.you.sure": "Czy na pewno?", "prompt.are.you.sure": "Czy na pewno?",
"title.unknown.group": "Nieznana grupa", "title.unknown.group": "Nieznana grupa",
"title.unknown": "Nieznany", "title.unknown": "Nieznany",
@ -269,10 +262,5 @@
"action.leaving.group": "Opuszczanie grupy...", "action.leaving.group": "Opuszczanie grupy...",
"action.key.rotating": "Obracanie kluczy...", "action.key.rotating": "Obracanie kluczy...",
"action.key.rotation": "Rotacja kluczy", "action.key.rotation": "Rotacja kluczy",
"desc.leave.group.confirm": "Czy na pewno chcesz opuścić tę grupę?", "desc.leave.group.confirm": "Czy na pewno chcesz opuścić tę grupę?"
"desc.key.rotation.you": "Wygenerujesz nowy klucz szyfrujący dla tej grupy.",
"label.keys.to.encrypt": "kluczy do zaszyfrowania",
"action.group.creating": "Tworzenie grupy...",
"title.group.settings": "Ustawienia grupy",
"title.leave.group": "Opuść grupę"
} }

View file

@ -30,10 +30,11 @@ const ICONS = {
pin: (props) => ICONS._material(`<path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"/>`, props), pin: (props) => ICONS._material(`<path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"/>`, props),
call: (props) => ICONS._material(`<path d="M798-120q-125 0-247-54.5T329-329Q229-429 174.5-551T120-798q0-18 12-30t30-12h162q14 0 25 9.5t13 22.5l26 140q2 16-1 27t-11 19l-97 98q20 37 47.5 71.5T387-386q31 31 65 57.5t72 48.5l94-94q9-9 23.5-13.5T670-390l138 28q14 4 23 14.5t9 23.5v162q0 18-12 30t-30 12ZM241-600l66-66-17-94h-89q5 41 14 81t26 79Zm358 358q39 17 79.5 27t81.5 13v-88l-94-19-67 67ZM241-600Zm358 358Z"/>`, props), call: (props) => ICONS._material(`<path d="M798-120q-125 0-247-54.5T329-329Q229-429 174.5-551T120-798q0-18 12-30t30-12h162q14 0 25 9.5t13 22.5l26 140q2 16-1 27t-11 19l-97 98q20 37 47.5 71.5T387-386q31 31 65 57.5t72 48.5l94-94q9-9 23.5-13.5T670-390l138 28q14 4 23 14.5t9 23.5v162q0 18-12 30t-30 12ZM241-600l66-66-17-94h-89q5 41 14 81t26 79Zm358 358q39 17 79.5 27t81.5 13v-88l-94-19-67 67ZM241-600Zm358 358Z"/>`, props),
account: (props) => ICONS._material(`<path d="M200-200h560v-560H200v560Zm0 80q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-160h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/>`, props), account: (props) => ICONS._material(`<path d="M200-200h560v-560H200v560Zm0 80q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-160h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/>`, props),
settings: (props) => ICONS._material(`<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 167-116.5 283.5T480-80Zm0-157q102 0 174-72t72-174q0-102-72-174t-174-72q-102 0-174 72t-72 174q0 102 72 174t174 72Zm0-80q-68 0-116.5-48.5T315-482q0-68 48.5-116.5T480-647q68 0 116.5 48.5T645-482q0 68-48.5 116.5T480-317Zm0-60q43 0 74-31t31-74q0-43-31-74t-74-31q-43 0-74 31t-31 74q0 43 31 74t74 31Zm0-105Z"/>`, props),
profile: (props) => ICONS._material(`<path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/>`, props), profile: (props) => ICONS._material(`<path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/>`, props),
replyAction: (props) => ICONS._material(`<path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/>`, props), replyAction: (props) => ICONS._material(`<path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/>`, props),
react: (props) => ICONS._material(`<path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>`, props), react: (props) => ICONS._material(`<path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>`, props),
settings: (props) => ICONS._material(`<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/>`, props), settingsProfile: (props) => ICONS._material(`<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/>`, props),
download: (props) => ICONS._feather(`<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>`, props), download: (props) => ICONS._feather(`<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>`, props),
camera: (props) => ICONS._feather(`<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle>`, props), camera: (props) => ICONS._feather(`<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle>`, props),

View file

@ -256,12 +256,9 @@
const roomKey = randomRoomKey256Chars(); const roomKey = randomRoomKey256Chars();
const selfWrapped = await wrapRoomKeyForSelf(roomKey); const selfWrapped = await wrapRoomKeyForSelf(roomKey);
let historyVisible = document.getElementById("creategroup-history") ? document.getElementById("creategroup-history").checked.toString() : "false";
let payload = JSON.stringify({ let payload = JSON.stringify({
string1: groupName, string1: groupName,
string2: selfWrapped, string2: selfWrapped
string3: historyVisible
}); });
let res = await fetchEncrypted("group/create", payload); let res = await fetchEncrypted("group/create", payload);

View file

@ -432,7 +432,7 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey,
let popupContainer = document.getElementById("generic-popup-container"); let popupContainer = document.getElementById("generic-popup-container");
let popupInner = document.getElementById("generic-popup-inner"); let popupInner = document.getElementById("generic-popup-inner");
let confirmClass = isDestructive ? "popup-btn-danger" : ""; let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm";
genericPopupCancelCallback = onCancel; genericPopupCancelCallback = onCancel;
@ -449,10 +449,10 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey,
${descHtml} ${descHtml}
</div> </div>
<div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;"> <div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;">
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()"> <button class="profile-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
<blah>${cancelActionKey}</blah> <blah>${cancelActionKey}</blah>
</button> </button>
<button class="profile-action-btn popup-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()"> <button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
<blah>${confirmActionKey}</blah> <blah>${confirmActionKey}</blah>
</button> </button>
</div> </div>
@ -474,7 +474,7 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c
let popupContainer = document.getElementById("generic-popup-container"); let popupContainer = document.getElementById("generic-popup-container");
let popupInner = document.getElementById("generic-popup-inner"); let popupInner = document.getElementById("generic-popup-inner");
let confirmClass = isDestructive ? "popup-btn-danger" : ""; let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm";
genericPopupCancelCallback = onCancel; genericPopupCancelCallback = onCancel;
@ -491,15 +491,12 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c
<div style="width: 100%; text-align: center; color: var(--text-color); opacity: 0.8; margin-top: 0.5rem; margin-bottom: 1rem;"> <div style="width: 100%; text-align: center; color: var(--text-color); opacity: 0.8; margin-top: 0.5rem; margin-bottom: 1rem;">
${descHtml} ${descHtml}
</div> </div>
<div class="input-group" style="margin: 0 auto 1.5rem auto; width: 100%;"> <input type="text" id="generic-popup-input" class="forminput" placeholder="{blah(${placeholderKey})}" style="width: 100%; margin-bottom: 1rem; box-sizing: border-box;" onkeydown="if(arguments[0].key === 'Enter') { if(genericPopupConfirmCallback) genericPopupConfirmCallback(); }">
<label for="generic-popup-input" style="margin-left: calc((var(--button-margin) * 2) - 0.6rem);"><blah>${placeholderKey}</blah></label>
<input type="text" id="generic-popup-input" class="forminput" placeholder="${processBlah(placeholderKey)}" style="margin-left: 0; margin-right: 0; width: 100%; box-sizing: border-box;" onkeydown="if(arguments[0].key === 'Enter') { if(genericPopupConfirmCallback) genericPopupConfirmCallback(); }">
</div>
<div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;"> <div style="display: flex; gap: 0.5rem; justify-content: center; width: 100%;">
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()"> <button class="profile-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
<blah>${cancelActionKey}</blah> <blah>${cancelActionKey}</blah>
</button> </button>
<button class="profile-action-btn popup-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()"> <button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
<blah>${confirmActionKey}</blah> <blah>${confirmActionKey}</blah>
</button> </button>
</div> </div>
@ -534,7 +531,7 @@ function closeRotationPopup() {
async function showRotationPopup(groupId, originalMessagePayload) { async function showRotationPopup(groupId, originalMessagePayload) {
pendingRotationMessagePayload = originalMessagePayload; pendingRotationMessagePayload = originalMessagePayload;
showAction("action.key.rotating", "key_rotation"); showAction("action.key.rotation", "key_rotation");
try { try {
let membersRaw = await fetchEncrypted("group/members", groupId); let membersRaw = await fetchEncrypted("group/members", groupId);
@ -542,7 +539,7 @@ async function showRotationPopup(groupId, originalMessagePayload) {
let members = JSON.parse(membersRaw); let members = JSON.parse(membersRaw);
let descHtml = `<blah>desc.key.rotation.you</blah><br><br><b>${members.length} <blah>label.keys.to.encrypt</blah></b>`; let descHtml = `<blah>desc.key.rotation.info</blah><br><br><b>${members.length} <blah>label.members</blah></b>`;
showConfirmPopup( showConfirmPopup(
"title.key.rotation", "title.key.rotation",
@ -560,7 +557,6 @@ async function showRotationPopup(groupId, originalMessagePayload) {
async function executeKeyRotation() { async function executeKeyRotation() {
closeRotationPopup(); closeRotationPopup();
clearAction("key_rotation");
showAction("action.key.rotating", "key_rotation_exec"); showAction("action.key.rotating", "key_rotation_exec");
try { try {
@ -617,12 +613,29 @@ async function executeKeyRotation() {
clearAction("key_rotation_exec"); clearAction("key_rotation_exec");
showBlahNotification("success:key.rotated"); 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; pendingRotationMessagePayload = null;
} catch(e) { } catch(e) {
clearAction("key_rotation_exec"); clearAction("key_rotation_exec");
showBlahNotification(e.message || e); console.error(e);
pendingRotationMessagePayload = null; showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong");
} }
} }
@ -2128,7 +2141,7 @@ async function renderInvites(res, type) {
let actions = ""; let actions = "";
let desc = ""; let desc = "";
let displayName = username || onlyId; let displayName = username.split(':')[0] || onlyId;
if (type === "received") { if (type === "received") {
desc = `:desc.invite.dm.received:${displayName};${onlyId}`; desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
actions = ` actions = `
@ -2182,7 +2195,7 @@ async function renderInvites(res, type) {
let actions = ""; let actions = "";
let desc = ""; let desc = "";
let displayName = username || onlyId; let displayName = username.split(':')[0] || onlyId;
let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`; let val1 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${displayName}</b></span>`;
let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`; let val2 = `<span onclick="openProfile('${targetId}')" style="cursor:pointer"><b>${onlyId}</b></span>`;
@ -2386,13 +2399,6 @@ function gotoJoinSpace() {
} }
async function gotoHome(splash = true) { async function gotoHome(splash = true) {
if (dmMessagePollInterval) {
clearInterval(dmMessagePollInterval);
dmMessagePollInterval = null;
}
currentDmId = null;
window.currentRoomType = null;
if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar
setActiveSidebarIndicator(sidebarHomeIndicator); setActiveSidebarIndicator(sidebarHomeIndicator);
if (splash) { if (splash) {
@ -2585,8 +2591,6 @@ async function renderDms(res) {
let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27"); let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27");
let unreadBadge = `<div class="unread-badge ${isRead ? "" : "show"}"></div>`;
html += ` html += `
<button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', decodeURIComponent('${safeTargetUsername}'), '${targetId}')"> <button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', decodeURIComponent('${safeTargetUsername}'), '${targetId}')">
<img src="${pfp}" class="room-pfp"> <img src="${pfp}" class="room-pfp">
@ -2594,7 +2598,6 @@ async function renderDms(res) {
<span class="room-name" style="${nameStyle}">${displayName}</span> <span class="room-name" style="${nameStyle}">${displayName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername} (${targetId.split(';')[0]})</span> <span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername} (${targetId.split(';')[0]})</span>
</div> </div>
${unreadBadge}
</button> </button>
`; `;
count++; count++;
@ -2648,8 +2651,6 @@ async function renderGroups(res) {
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27"); let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
let pfp = await getGroupAvatarUrl(groupId, groupName); let pfp = await getGroupAvatarUrl(groupId, groupName);
let unreadBadge = `<div class="unread-badge ${isRead ? "" : "show"}"></div>`;
html += ` html += `
<button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')" oncontextmenu="handleGroupContextMenu(event, '${groupId}')"> <button class="room-entry" id="group-btn-${groupId}" onclick="openDm('${groupId}', decodeURIComponent('${safeGroupName}'), '${groupId}', 'groups')" oncontextmenu="handleGroupContextMenu(event, '${groupId}')">
<img src="${pfp}" class="room-pfp"> <img src="${pfp}" class="room-pfp">
@ -2657,7 +2658,6 @@ async function renderGroups(res) {
<span class="room-name" style="${nameStyle}">${groupName}</span> <span class="room-name" style="${nameStyle}">${groupName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span> <span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${groupId.replace(';', ':')}</span>
</div> </div>
${unreadBadge}
</button> </button>
`; `;
count++; count++;
@ -2742,7 +2742,7 @@ async function inviteToGroupPrompt(groupId) {
showPromptPopup( showPromptPopup(
"action.invite", "action.invite",
"", "",
"title.username", "prompt.invite.username",
"action.invite", "action.invite",
"action.cancel", "action.cancel",
async (targetUsername) => { async (targetUsername) => {
@ -2762,63 +2762,9 @@ async function inviteToGroupPrompt(groupId) {
try { try {
showAction("action.invite.sending", "invite.action"); showAction("action.invite.sending", "invite.action");
let keysPayload = "";
try {
let infoRes = await fetchEncrypted("group/info", groupId);
if (infoRes && !infoRes.startsWith("error:")) {
let info = JSON.parse(infoRes);
if (info.string6 === "true") { // historyVisible
let lastKeyIndex = parseInt(info.string4);
if (!isNaN(lastKeyIndex)) {
let publicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${targetId}`);
if (publicKeysRaw && publicKeysRaw.length > 10) {
let publicKeys = JSON.parse(publicKeysRaw);
let hybrid = await deriveHybridKeyToUser(publicKeys);
let myPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${window.id}`);
let keysDict = {};
let allKeysSuccess = true;
for (let i = 0; i <= lastKeyIndex; i++) {
let roomKey = await getGroupKey(groupId, i.toString());
if (!roomKey) {
allKeysSuccess = false;
break;
}
let encForMember = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes);
let setupPayloadJson = JSON.stringify({
v: 1,
alg: "hybrid-x25519-mlkem768",
ctMlKem: hybrid.ciphertextMlKemBase64,
enc: encForMember
});
keysDict[i.toString()] = `SETUP:${myPublicKeysRaw};${setupPayloadJson}`;
}
if (!allKeysSuccess) {
clearAction("invite.action");
showBlahNotification("error:failed.to.prepare.keys");
return;
}
keysPayload = JSON.stringify(keysDict);
} else {
clearAction("invite.action");
showBlahNotification("error:failed.to.prepare.keys");
return;
}
}
}
}
} catch (e) {
console.error("Error generating keys payload for invite", e);
clearAction("invite.action");
showBlahNotification("error:failed.to.prepare.keys");
return;
}
let payload = JSON.stringify({ let payload = JSON.stringify({
string1: groupId, string1: groupId,
string2: targetId, string2: targetId
string3: keysPayload
}); });
let res = await fetchEncrypted("group/invite", payload); let res = await fetchEncrypted("group/invite", payload);
@ -2911,10 +2857,10 @@ async function openDm(dmId, username, targetId, type = 'dms') {
let iconHtml = ""; let iconHtml = "";
if (type === 'dms') { if (type === 'dms') {
let pfp = await getAvatarUrl(targetId, username); let pfp = await getAvatarUrl(targetId, username);
iconHtml = `<img src="${pfp}" style="width: 2.3rem;height: 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 { } else {
let pfp = await getGroupAvatarUrl(dmId, username); let pfp = await getGroupAvatarUrl(dmId, username);
iconHtml = `<img src="${pfp}" style="width: 2.3rem;height: 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);">`;
} }
if (currentRoomsBarTitle !== "title.home") { if (currentRoomsBarTitle !== "title.home") {
@ -3072,27 +3018,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
if (atts.length > 0) { if (atts.length > 0) {
attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">`; attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">`;
for (let att of atts) { for (let att of atts) {
if (att.enc && att.enc.startsWith("ENC:")) {
try {
if (keyToUse) {
let keyBytes = await sha256Bytes(base64ToUint8(keyToUse));
let decStr = await decryptAesGcmFromBase64(att.enc.substring(4), keyBytes);
let metadata = JSON.parse(decStr);
att.name = metadata.name || "unknown";
att.type = metadata.type || "application/octet-stream";
att.size = metadata.size || 0;
} else {
att.name = processBlah("error:messages.decrypt.failed");
att.type = "application/octet-stream";
}
} catch(e) {
att.name = processBlah("error:messages.decrypt.failed");
att.type = "application/octet-stream";
}
}
let attContainerId = `att-container-${msgId}-${att.id}`; let attContainerId = `att-container-${msgId}-${att.id}`;
let isMedia = att.type && (att.type.startsWith("image/") || att.type.startsWith("video/")); let isMedia = att.type.startsWith("image/") || att.type.startsWith("video/");
let sizeStr = att.size ? formatBytes(att.size) : ""; let sizeStr = att.size ? formatBytes(att.size) : "";
let displayName = truncateFilename(att.name, 25); let displayName = truncateFilename(att.name, 25);
let iconSvg = isMedia let iconSvg = isMedia
@ -3133,7 +3060,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
content = processBlah(content); content = processBlah(content);
if (msg.type && msg.type.startsWith("larp.info")) { if (msg.type && msg.type.startsWith("larp.info")) {
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" || msg.type === "larp.info.history") { 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"); let authorName = processBlah("title.unknown");
if (msg.author && msg.author !== "0") { if (msg.author && msg.author !== "0") {
if (!dmUsernameCache[msg.author]) { if (!dmUsernameCache[msg.author]) {
@ -3156,7 +3083,6 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
else if (msg.type === "larp.info.join") actionStr = "group.user.joined.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.leave") actionStr = "group.user.left.by";
else if (msg.type === "larp.info.create") actionStr = "group.created.by"; else if (msg.type === "larp.info.create") actionStr = "group.created.by";
else if (msg.type === "larp.info.history") actionStr = "group.history.changed.by";
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`; let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
@ -3164,8 +3090,6 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) { if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) {
let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`; let clickableMsg = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${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); content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg);
} else if (msg.type === "larp.info.history") {
content = textStr.replace("{0}", clickableAuthor).replace("{1}", content);
} else { } else {
content = textStr.replace("{0}", clickableAuthor); content = textStr.replace("{0}", clickableAuthor);
} }
@ -3597,10 +3521,7 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name,
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (window.currentRoomType === 'groups' && window.groupKeysCache[currentDmId]) { container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading</div>`;
delete window.groupKeysCache[currentDmId][keyIndex];
}
container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading - click to retry</div>`;
container.dataset.loading = "false"; container.dataset.loading = "false";
} }
} }
@ -3727,17 +3648,12 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
} }
if (decText && decText.startsWith("success:")) { if (decText && decText.startsWith("success:")) {
let metadataObj = {
type: file.type,
name: file.name,
size: file.size
};
let encMetadata = "ENC:" + await encryptAesGcmToBase64(JSON.stringify(metadataObj), dmKeyBytes);
attachmentsPayload.push({ attachmentsPayload.push({
id: i, id: i,
staging_id: decText.split(":")[1], type: file.type,
enc: encMetadata name: file.name,
size: file.size,
staging_id: decText.split(":")[1]
}); });
} else { } else {
throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed"); throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed");
@ -3755,11 +3671,9 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
if (window.currentRoomType === 'groups') { if (window.currentRoomType === 'groups') {
msgPayload.string5 = window.currentGroupKeyIndex || "0"; msgPayload.string5 = window.currentGroupKeyIndex || "0";
msgPayload.rawPlainContent = content; // for retry
} }
let savedContentForRestore = content;
let savedAttachmentsForRestore = [...currentDraftAttachments];
input.value = ""; input.value = "";
input.style.height = '2.5rem'; input.style.height = '2.5rem';
sendbutton.style.height = '2.5rem'; sendbutton.style.height = '2.5rem';
@ -3779,13 +3693,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') { if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') {
let pElem = document.getElementById(pendingMsgId); let pElem = document.getElementById(pendingMsgId);
if (pElem) pElem.remove(); if (pElem) pElem.remove();
showRotationPopup(currentDmId, msgPayload);
// Restore input and attachments for the user to retry manually
input.value = savedContentForRestore;
currentDraftAttachments = savedAttachmentsForRestore;
renderAttachmentsPreview();
showRotationPopup(currentDmId, null);
} else if (res && res.startsWith("error:")) { } else if (res && res.startsWith("error:")) {
showBlahNotification(res); showBlahNotification(res);
let pElem = document.getElementById(pendingMsgId); let pElem = document.getElementById(pendingMsgId);
@ -4374,12 +4282,7 @@ async function changePassword() {
} }
async function deleteAccount() { async function deleteAccount() {
showConfirmPopup( if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return;
"title.delete.account",
"<blah>desc.delete.account</blah><br><br><blah>prompt.are.you.sure</blah>",
"action.delete.account",
"action.cancel",
async () => {
showAction("action.account.deleting", "accountdel"); showAction("action.account.deleting", "accountdel");
let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, "");
clearAction("accountdel"); clearAction("accountdel");
@ -4390,10 +4293,6 @@ async function deleteAccount() {
} else { } else {
showBlahNotification(text); showBlahNotification(text);
} }
},
null,
true
);
} }
async function saveProfile() { async function saveProfile() {
@ -4450,8 +4349,8 @@ function initCropper() {
let zoomSlider = document.getElementById('cropper-zoom'); let zoomSlider = document.getElementById('cropper-zoom');
// Set aspect ratio // Set aspect ratio
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494; let targetWidth = cropperType === 'avatar' ? 512 : 494;
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200; let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
let aspectRatio = targetWidth / targetHeight; let aspectRatio = targetWidth / targetHeight;
container.style.height = 'auto'; container.style.height = 'auto';
@ -4519,8 +4418,8 @@ async function confirmCropper() {
let zoom = parseFloat(document.getElementById('cropper-zoom').value); let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let container = document.getElementById('cropper-viewport-container'); let container = document.getElementById('cropper-viewport-container');
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988; let targetWidth = cropperType === 'avatar' ? 512 : 988;
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 400; let targetHeight = cropperType === 'avatar' ? 512 : 400;
let scaleRatio = targetWidth / container.clientWidth; let scaleRatio = targetWidth / container.clientWidth;
let drawX = cropperPosX * scaleRatio; let drawX = cropperPosX * scaleRatio;
let drawY = cropperPosY * scaleRatio; let drawY = cropperPosY * scaleRatio;
@ -4691,20 +4590,10 @@ async function confirmCropper() {
newSrc = 'data:image/webp;base64,' + b64; newSrc = 'data:image/webp;base64,' + b64;
} }
let payloadKey = (cropperType === 'avatar' || cropperType === 'group-avatar') ? 'larp.profile.pfp' : 'larp.profile.banner'; let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
showAction("action.uploading", "uploadmedia");
let res;
if (cropperType === 'group-avatar') {
let payload = JSON.stringify({
string1: window.currentSettingsGroupId,
string2: payloadKey,
string3: b64
});
res = await fetchEncrypted("group/storage/public/update", payload);
} else {
let fileBytes = base64ToUint8(b64); let fileBytes = base64ToUint8(b64);
res = await fetchEncryptedUpload(payloadKey, fileBytes); showAction("action.uploading", "uploadmedia");
} let res = await fetchEncryptedUpload(payloadKey, fileBytes);
clearAction("uploadmedia"); clearAction("uploadmedia");
if (res && res.startsWith("error:")) { if (res && res.startsWith("error:")) {
showBlahNotification(res); showBlahNotification(res);
@ -4714,15 +4603,9 @@ async function confirmCropper() {
// Update all matching images on the page instantly // Update all matching images on the page instantly
document.querySelectorAll('img').forEach(img => { document.querySelectorAll('img').forEach(img => {
if (cropperType === 'group-avatar') {
if (img.src.includes(`id=${window.currentSettingsGroupId}&e=${payloadKey}`) || img.id === 'group-settings-avatar-preview') {
img.src = newSrc;
}
} else {
if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) { if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) {
img.src = newSrc; img.src = newSrc;
} }
}
}); });
} }
@ -4790,11 +4673,6 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
if (nameSpan) { if (nameSpan) {
nameSpan.style.opacity = isRead ? "0.8" : "1"; nameSpan.style.opacity = isRead ? "0.8" : "1";
} }
let badge = btn.querySelector('.unread-badge');
if (badge) {
if (isRead) badge.classList.remove("show");
else badge.classList.add("show");
}
if (timestamp) { if (timestamp) {
container.prepend(btn); container.prepend(btn);
} }
@ -4827,11 +4705,6 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
if (nameSpan) { if (nameSpan) {
nameSpan.style.opacity = isRead ? "0.8" : "1"; nameSpan.style.opacity = isRead ? "0.8" : "1";
} }
let badge = btn.querySelector('.unread-badge');
if (badge) {
if (isRead) badge.classList.remove("show");
else badge.classList.add("show");
}
if (timestamp) { if (timestamp) {
container.prepend(btn); container.prepend(btn);
} }
@ -5054,48 +4927,9 @@ function handleGroupContextMenu(e, groupId) {
fixedContextMenu.style.top = `${newTop}px`; fixedContextMenu.style.top = `${newTop}px`;
} }
async function gotoGroupSettings(groupId) { function gotoGroupSettings(groupId) {
if (fixedContextMenu) fixedContextMenu.classList.remove("show"); if (fixedContextMenu) fixedContextMenu.classList.remove("show");
// Not implemented yet
window.currentSettingsGroupId = groupId;
let infoStr = await fetchEncrypted("group/info", groupId);
if (!infoStr.startsWith("error:")) {
let info = JSON.parse(infoStr);
await switchRoomContent("title.group.settings", groupSettingsScreen, false);
document.getElementById('group-settings-name').value = info.string2;
let pfpUrl = await getGroupAvatarUrl(groupId, info.string2);
document.getElementById('group-settings-avatar-preview').src = pfpUrl;
document.getElementById('group-settings-history').checked = (info.string6 === "true");
} else {
showBlahNotification(infoStr);
}
}
async function saveGroupSettings() {
let groupId = window.currentSettingsGroupId;
let name = document.getElementById('group-settings-name').value;
let historyVisible = document.getElementById('group-settings-history').checked.toString();
let payload = JSON.stringify({
string1: groupId,
string2: name,
string3: historyVisible
});
showAction("action.saving", "savegroup");
let res = await fetchEncrypted("group/settings/update", payload);
clearAction("savegroup");
if (res === "success") {
showBlahNotification("success:group.settings.saved");
refreshGroups(0, false);
} else {
showBlahNotification(res);
}
} }
function leaveGroupPrompt(groupId) { function leaveGroupPrompt(groupId) {

View file

@ -31,18 +31,8 @@ var createGroupScreen = `
<div class="input-group"> <div class="input-group">
<label for="creategroup-name"><blah>title.name</blah></label> <label for="creategroup-name"><blah>title.name</blah></label>
<input type="text" id="creategroup-name" placeholder="{blah(placeholder.create.group.input)}" class="forminput"> <input type="text" id="creategroup-name" placeholder="{blah(placeholder.create.group.input)}" class="forminput">
<div class="setting-row" style="text-align: left;">
<div class="setting-info">
<span class="setting-title"><blah>title.group.history.visible</blah></span>
<span class="setting-desc"><blah>desc.group.history.visible</blah></span>
</div> </div>
<label class="toggle-switch"> <div class="input-group">
<input type="checkbox" id="creategroup-history">
<span class="toggle-slider"></span>
</label>
</div>
<button class="submit-button blah" onclick="createGroup()">title.create.group</button> <button class="submit-button blah" onclick="createGroup()">title.create.group</button>
</div> </div>
</div> </div>
@ -221,7 +211,7 @@ var profileContextMenu = `
<span class="blah">title.profile</span> <span class="blah">title.profile</span>
</button> </button>
<button onclick="gotoSettings()"> <button onclick="gotoSettings()">
${ICONS.settings()} ${ICONS.settingsProfile()}
<span class="blah">title.settings</span> <span class="blah">title.settings</span>
</button> </button>
<button onclick="logout()"> <button onclick="logout()">
@ -337,7 +327,7 @@ var accountSettings = `
<div class="input-group"> <div class="input-group">
<label style="color: var(--big-red);"><blah>title.delete.account</blah></label> <label style="color: var(--big-red);"><blah>title.delete.account</blah></label>
<p style="font-size: 0.85rem; opacity: 0.7; margin-bottom: 0.5rem; margin-left: 0.6rem;"><blah>desc.delete.account</blah></p> <p style="font-size: 0.85rem; opacity: 0.7; margin-bottom: 0.5rem; margin-left: 0.6rem;"><blah>desc.delete.account</blah></p>
<button class="submit-button popup-btn-danger blah" onclick="deleteAccount()">action.delete.account</button> <button class="submit-button blah" onclick="deleteAccount()" style="background: var(--big-red); color: white;">action.delete.account</button>
</div> </div>
</div> </div>
`; `;
@ -378,36 +368,6 @@ var profileSettings = `
</div> </div>
</div> </div>
`; `;
var groupSettingsScreen = `
<div class="auth-box popup-auto-height" style="margin-top:2rem">
<div class="input-group" style="display: flex; flex-direction: row; align-items: stretch; justify-content: space-between; border: var(--border-width) solid var(--light-border-color); border-radius: 0.8rem; padding: 1rem; width: 100%; max-width: 30rem;">
<img id="group-settings-avatar-preview" src="" style="width: 4.5rem; height: 4.5rem; border-radius: 0.8rem; border: var(--border-width) solid rgba(255, 255, 255, 0.2); object-fit: cover; background: var(--main-bg-color);">
<div style="display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; padding: 0.1rem 0;">
<div style="font-weight: bold;"><blah>title.avatar</blah></div>
<button class="submit-button blah" style="margin: 0; padding: 0.5rem 1rem; width: auto;" onclick="document.getElementById('group-settings-avatar-input').click()">action.change.avatar</button>
<input type="file" id="group-settings-avatar-input" style="display: none;" accept="image/*" onchange="openCropper(this, 'group-avatar')">
</div>
</div>
<br>
<div class="input-group">
<label for="group-settings-name"><blah>title.group.name</blah></label>
<input type="text" id="group-settings-name" class="forminput">
<div class="setting-row">
<div class="setting-info">
<span class="setting-title"><blah>title.group.history.visible</blah></span>
<span class="setting-desc"><blah>desc.group.history.visible</blah></span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="group-settings-history">
<span class="toggle-slider"></span>
</label>
</div>
<button class="submit-button blah" onclick="saveGroupSettings()">action.save</button>
</div>
</div>
`;
var appearanceSettings = ` var appearanceSettings = `
<div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;"> <div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;">

View file

@ -366,7 +366,6 @@ herotitle {
gap: 0.2rem; gap: 0.2rem;
width: 90%; width: 90%;
max-width: 30rem; max-width: 30rem;
margin: 0 auto;
} }
.input-group label { .input-group label {
font-size: 0.75rem; font-size: 0.75rem;
@ -879,27 +878,35 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
height: auto; height: auto;
max-height: 85vh; max-height: 85vh;
} }
button.profile-action-btn.popup-action-btn { .popup-btn-cancel {
flex: 1;
display: flex;
justify-content: center;
margin-top: 0;
}
button.profile-action-btn.popup-btn-cancel {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
color: var(--text-color); color: var(--text-color);
border: var(--border-width) solid var(--light-border-color); border: var(--border-width) solid var(--light-border-color);
flex: 1;
justify-content: center;
} }
button.profile-action-btn.popup-btn-cancel:hover { .popup-btn-cancel:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
} }
button.profile-action-btn.popup-btn-danger, button.submit-button.popup-btn-danger { .popup-btn-confirm {
background: var(--big-red) !important; background: var(--text-color);
color: white !important; color: var(--main-bg-color);
border: none; border: none;
flex: 1;
justify-content: center;
} }
button.profile-action-btn.popup-btn-danger:hover, button.submit-button.popup-btn-danger:hover { .popup-btn-confirm:hover {
filter: brightness(0.85); background: white;
}
.popup-btn-danger {
background: var(--big-red);
color: white;
border: none;
flex: 1;
justify-content: center;
}
.popup-btn-danger:hover {
background: #ff5252;
} }
.profile-popup-container.show .profile-popup-content { .profile-popup-container.show .profile-popup-content {
transform: scale(1); transform: scale(1);
@ -1065,106 +1072,3 @@ body.hide-topbar-pin #roomdetailsbar roomtopbar .room-action-pin { display: flex
body.hide-topbar-call roomcontent roomtopbar .room-action-call { display: none !important; } body.hide-topbar-call roomcontent roomtopbar .room-action-call { display: none !important; }
body.hide-topbar-call #roomdetailsbar roomtopbar .room-action-call { display: flex !important; } body.hide-topbar-call #roomdetailsbar roomtopbar .room-action-call { display: flex !important; }
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 3.4rem;
height: 1.8rem;
margin: var(--button-margin);
flex-shrink: 0;
}
label.toggle-switch {
opacity: 0.8;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .4s;
border-radius: 1.8rem;
border: var(--border-width) solid var(--light-border-color);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.2rem;
width: 1.2rem;
left: 0.25rem;
top: 50%;
transform: translateY(-50%);
background-color: var(--text-color);
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--text-color) !important;
border-color: var(--text-color) !important;
}
input:checked + .toggle-slider:before {
transform: translate(1.6rem, -50%);
background-color: var(--main-bg-color) !important;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
width: auto;
box-sizing: border-box;
margin: var(--button-margin);
padding: 0.5rem;
border-radius: 0.8rem;
background-color: rgba(255, 255, 255, 0.02);
}
.setting-row:hover {
background-color: rgba(255, 255, 255, 0.04);
}
.setting-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.setting-title {
font-size: 1.1rem;
font-weight: bold;
}
.setting-desc {
font-size: 0.85rem;
color: var(--darker-text-color);
margin-top: 0.2rem;
}
.unread-badge {
width: 0.65rem;
height: 0.65rem;
min-width: 0.65rem;
min-height: 0.65rem;
background-color: var(--big-red);
border-radius: 50%;
flex-shrink: 0;
margin-right: 0.5rem;
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform: scale(0);
}
.unread-badge.show {
transform: scale(1);
animation: badge-appear 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes badge-appear {
0% { transform: scale(0); }
100% { transform: scale(1); }
}