Compare commits
3 commits
113530acb8
...
55760ba368
| Author | SHA1 | Date | |
|---|---|---|---|
| 55760ba368 | |||
| 08ff4755a1 | |||
| bb135bed65 |
16 changed files with 858 additions and 192 deletions
|
|
@ -262,5 +262,10 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +249,13 @@
|
||||||
"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",
|
||||||
|
|
@ -262,5 +269,10 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +248,13 @@
|
||||||
"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",
|
||||||
|
|
@ -262,5 +269,10 @@
|
||||||
"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ę"
|
||||||
}
|
}
|
||||||
|
|
@ -30,11 +30,10 @@ 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),
|
||||||
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),
|
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),
|
||||||
|
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -256,9 +256,12 @@
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -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" : "popup-btn-confirm";
|
let confirmClass = isDestructive ? "popup-btn-danger" : "";
|
||||||
|
|
||||||
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-btn-cancel" onclick="closeGenericPopup()">
|
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
|
||||||
<blah>${cancelActionKey}</blah>
|
<blah>${cancelActionKey}</blah>
|
||||||
</button>
|
</button>
|
||||||
<button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
|
<button class="profile-action-btn popup-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" : "popup-btn-confirm";
|
let confirmClass = isDestructive ? "popup-btn-danger" : "";
|
||||||
|
|
||||||
genericPopupCancelCallback = onCancel;
|
genericPopupCancelCallback = onCancel;
|
||||||
|
|
||||||
|
|
@ -491,12 +491,15 @@ 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>
|
||||||
<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(); }">
|
<div class="input-group" style="margin: 0 auto 1.5rem auto; width: 100%;">
|
||||||
|
<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-btn-cancel" onclick="closeGenericPopup()">
|
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
|
||||||
<blah>${cancelActionKey}</blah>
|
<blah>${cancelActionKey}</blah>
|
||||||
</button>
|
</button>
|
||||||
<button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
|
<button class="profile-action-btn popup-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
|
||||||
<blah>${confirmActionKey}</blah>
|
<blah>${confirmActionKey}</blah>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -531,7 +534,7 @@ function closeRotationPopup() {
|
||||||
|
|
||||||
async function showRotationPopup(groupId, originalMessagePayload) {
|
async function showRotationPopup(groupId, originalMessagePayload) {
|
||||||
pendingRotationMessagePayload = originalMessagePayload;
|
pendingRotationMessagePayload = originalMessagePayload;
|
||||||
showAction("action.key.rotation", "key_rotation");
|
showAction("action.key.rotating", "key_rotation");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let membersRaw = await fetchEncrypted("group/members", groupId);
|
let membersRaw = await fetchEncrypted("group/members", groupId);
|
||||||
|
|
@ -539,7 +542,7 @@ async function showRotationPopup(groupId, originalMessagePayload) {
|
||||||
|
|
||||||
let members = JSON.parse(membersRaw);
|
let members = JSON.parse(membersRaw);
|
||||||
|
|
||||||
let descHtml = `<blah>desc.key.rotation.info</blah><br><br><b>${members.length} <blah>label.members</blah></b>`;
|
let descHtml = `<blah>desc.key.rotation.you</blah><br><br><b>${members.length} <blah>label.keys.to.encrypt</blah></b>`;
|
||||||
|
|
||||||
showConfirmPopup(
|
showConfirmPopup(
|
||||||
"title.key.rotation",
|
"title.key.rotation",
|
||||||
|
|
@ -557,6 +560,7 @@ 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 {
|
||||||
|
|
@ -613,29 +617,12 @@ 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");
|
||||||
console.error(e);
|
showBlahNotification(e.message || e);
|
||||||
showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong");
|
pendingRotationMessagePayload = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2141,7 +2128,7 @@ async function renderInvites(res, type) {
|
||||||
let actions = "";
|
let actions = "";
|
||||||
let desc = "";
|
let desc = "";
|
||||||
|
|
||||||
let displayName = username.split(':')[0] || onlyId;
|
let displayName = username || onlyId;
|
||||||
if (type === "received") {
|
if (type === "received") {
|
||||||
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
||||||
actions = `
|
actions = `
|
||||||
|
|
@ -2195,7 +2182,7 @@ async function renderInvites(res, type) {
|
||||||
let actions = "";
|
let actions = "";
|
||||||
let desc = "";
|
let desc = "";
|
||||||
|
|
||||||
let displayName = username.split(':')[0] || onlyId;
|
let displayName = username || 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>`;
|
||||||
|
|
||||||
|
|
@ -2399,6 +2386,13 @@ 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) {
|
||||||
|
|
@ -2591,6 +2585,8 @@ 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">
|
||||||
|
|
@ -2598,6 +2594,7 @@ 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++;
|
||||||
|
|
@ -2651,6 +2648,8 @@ 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">
|
||||||
|
|
@ -2658,6 +2657,7 @@ 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",
|
||||||
"",
|
"",
|
||||||
"prompt.invite.username",
|
"title.username",
|
||||||
"action.invite",
|
"action.invite",
|
||||||
"action.cancel",
|
"action.cancel",
|
||||||
async (targetUsername) => {
|
async (targetUsername) => {
|
||||||
|
|
@ -2762,9 +2762,63 @@ 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);
|
||||||
|
|
@ -2857,10 +2911,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; 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;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);">`;
|
||||||
} else {
|
} else {
|
||||||
let pfp = await getGroupAvatarUrl(dmId, username);
|
let pfp = await getGroupAvatarUrl(dmId, username);
|
||||||
iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
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);">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRoomsBarTitle !== "title.home") {
|
if (currentRoomsBarTitle !== "title.home") {
|
||||||
|
|
@ -3018,8 +3072,27 @@ 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.startsWith("image/") || att.type.startsWith("video/");
|
let isMedia = att.type && (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
|
||||||
|
|
@ -3060,7 +3133,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") {
|
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") {
|
||||||
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]) {
|
||||||
|
|
@ -3083,6 +3156,7 @@ 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>`;
|
||||||
|
|
||||||
|
|
@ -3090,6 +3164,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -3521,7 +3597,10 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name,
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading</div>`;
|
if (window.currentRoomType === 'groups' && window.groupKeysCache[currentDmId]) {
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3648,12 +3727,17 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decText && decText.startsWith("success:")) {
|
if (decText && decText.startsWith("success:")) {
|
||||||
attachmentsPayload.push({
|
let metadataObj = {
|
||||||
id: i,
|
|
||||||
type: file.type,
|
type: file.type,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size
|
||||||
staging_id: decText.split(":")[1]
|
};
|
||||||
|
let encMetadata = "ENC:" + await encryptAesGcmToBase64(JSON.stringify(metadataObj), dmKeyBytes);
|
||||||
|
|
||||||
|
attachmentsPayload.push({
|
||||||
|
id: i,
|
||||||
|
staging_id: decText.split(":")[1],
|
||||||
|
enc: encMetadata
|
||||||
});
|
});
|
||||||
} 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");
|
||||||
|
|
@ -3671,9 +3755,11 @@ 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';
|
||||||
|
|
@ -3693,7 +3779,13 @@ 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);
|
||||||
|
|
@ -4282,7 +4374,12 @@ async function changePassword() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAccount() {
|
async function deleteAccount() {
|
||||||
if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return;
|
showConfirmPopup(
|
||||||
|
"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");
|
||||||
|
|
@ -4293,6 +4390,10 @@ async function deleteAccount() {
|
||||||
} else {
|
} else {
|
||||||
showBlahNotification(text);
|
showBlahNotification(text);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
|
|
@ -4349,8 +4450,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' ? 512 : 494;
|
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494;
|
||||||
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
|
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200;
|
||||||
let aspectRatio = targetWidth / targetHeight;
|
let aspectRatio = targetWidth / targetHeight;
|
||||||
|
|
||||||
container.style.height = 'auto';
|
container.style.height = 'auto';
|
||||||
|
|
@ -4418,8 +4519,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' ? 512 : 988;
|
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988;
|
||||||
let targetHeight = cropperType === 'avatar' ? 512 : 400;
|
let targetHeight = cropperType === 'avatar' || cropperType === 'group-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;
|
||||||
|
|
@ -4590,10 +4691,20 @@ async function confirmCropper() {
|
||||||
newSrc = 'data:image/webp;base64,' + b64;
|
newSrc = 'data:image/webp;base64,' + b64;
|
||||||
}
|
}
|
||||||
|
|
||||||
let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
|
let payloadKey = (cropperType === 'avatar' || cropperType === 'group-avatar') ? 'larp.profile.pfp' : 'larp.profile.banner';
|
||||||
let fileBytes = base64ToUint8(b64);
|
|
||||||
showAction("action.uploading", "uploadmedia");
|
showAction("action.uploading", "uploadmedia");
|
||||||
let res = await fetchEncryptedUpload(payloadKey, fileBytes);
|
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);
|
||||||
|
res = await fetchEncryptedUpload(payloadKey, fileBytes);
|
||||||
|
}
|
||||||
clearAction("uploadmedia");
|
clearAction("uploadmedia");
|
||||||
if (res && res.startsWith("error:")) {
|
if (res && res.startsWith("error:")) {
|
||||||
showBlahNotification(res);
|
showBlahNotification(res);
|
||||||
|
|
@ -4603,9 +4714,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4673,6 +4790,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -4705,6 +4827,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -4927,9 +5054,48 @@ function handleGroupContextMenu(e, groupId) {
|
||||||
fixedContextMenu.style.top = `${newTop}px`;
|
fixedContextMenu.style.top = `${newTop}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoGroupSettings(groupId) {
|
async 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) {
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,18 @@ 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>
|
||||||
<div class="input-group">
|
<label class="toggle-switch">
|
||||||
|
<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>
|
||||||
|
|
@ -211,7 +221,7 @@ var profileContextMenu = `
|
||||||
<span class="blah">title.profile</span>
|
<span class="blah">title.profile</span>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="gotoSettings()">
|
<button onclick="gotoSettings()">
|
||||||
${ICONS.settingsProfile()}
|
${ICONS.settings()}
|
||||||
<span class="blah">title.settings</span>
|
<span class="blah">title.settings</span>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="logout()">
|
<button onclick="logout()">
|
||||||
|
|
@ -327,7 +337,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 blah" onclick="deleteAccount()" style="background: var(--big-red); color: white;">action.delete.account</button>
|
<button class="submit-button popup-btn-danger blah" onclick="deleteAccount()">action.delete.account</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -368,6 +378,36 @@ 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;">
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,7 @@ 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;
|
||||||
|
|
@ -878,35 +879,27 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
.popup-btn-cancel {
|
button.profile-action-btn.popup-action-btn {
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
.popup-btn-cancel:hover {
|
button.profile-action-btn.popup-btn-cancel:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
.popup-btn-confirm {
|
button.profile-action-btn.popup-btn-danger, button.submit-button.popup-btn-danger {
|
||||||
background: var(--text-color);
|
background: var(--big-red) !important;
|
||||||
color: var(--main-bg-color);
|
color: white !important;
|
||||||
border: none;
|
border: none;
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.popup-btn-confirm:hover {
|
button.profile-action-btn.popup-btn-danger:hover, button.submit-button.popup-btn-danger:hover {
|
||||||
background: white;
|
filter: brightness(0.85);
|
||||||
}
|
|
||||||
.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);
|
||||||
|
|
@ -1072,3 +1065,106 @@ 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); }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,5 +262,10 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +249,13 @@
|
||||||
"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",
|
||||||
|
|
@ -262,5 +269,10 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +248,13 @@
|
||||||
"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",
|
||||||
|
|
@ -262,5 +269,10 @@
|
||||||
"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ę"
|
||||||
}
|
}
|
||||||
|
|
@ -30,11 +30,10 @@ 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),
|
||||||
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),
|
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),
|
||||||
|
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -256,9 +256,12 @@
|
||||||
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);
|
||||||
|
|
|
||||||
272
webroot/main.js
272
webroot/main.js
|
|
@ -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" : "popup-btn-confirm";
|
let confirmClass = isDestructive ? "popup-btn-danger" : "";
|
||||||
|
|
||||||
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-btn-cancel" onclick="closeGenericPopup()">
|
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
|
||||||
<blah>${cancelActionKey}</blah>
|
<blah>${cancelActionKey}</blah>
|
||||||
</button>
|
</button>
|
||||||
<button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
|
<button class="profile-action-btn popup-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" : "popup-btn-confirm";
|
let confirmClass = isDestructive ? "popup-btn-danger" : "";
|
||||||
|
|
||||||
genericPopupCancelCallback = onCancel;
|
genericPopupCancelCallback = onCancel;
|
||||||
|
|
||||||
|
|
@ -491,12 +491,15 @@ 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>
|
||||||
<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(); }">
|
<div class="input-group" style="margin: 0 auto 1.5rem auto; width: 100%;">
|
||||||
|
<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-btn-cancel" onclick="closeGenericPopup()">
|
<button class="profile-action-btn popup-action-btn popup-btn-cancel" onclick="closeGenericPopup()">
|
||||||
<blah>${cancelActionKey}</blah>
|
<blah>${cancelActionKey}</blah>
|
||||||
</button>
|
</button>
|
||||||
<button class="profile-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
|
<button class="profile-action-btn popup-action-btn ${confirmClass}" onclick="if(genericPopupConfirmCallback) genericPopupConfirmCallback()">
|
||||||
<blah>${confirmActionKey}</blah>
|
<blah>${confirmActionKey}</blah>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -531,7 +534,7 @@ function closeRotationPopup() {
|
||||||
|
|
||||||
async function showRotationPopup(groupId, originalMessagePayload) {
|
async function showRotationPopup(groupId, originalMessagePayload) {
|
||||||
pendingRotationMessagePayload = originalMessagePayload;
|
pendingRotationMessagePayload = originalMessagePayload;
|
||||||
showAction("action.key.rotation", "key_rotation");
|
showAction("action.key.rotating", "key_rotation");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let membersRaw = await fetchEncrypted("group/members", groupId);
|
let membersRaw = await fetchEncrypted("group/members", groupId);
|
||||||
|
|
@ -539,7 +542,7 @@ async function showRotationPopup(groupId, originalMessagePayload) {
|
||||||
|
|
||||||
let members = JSON.parse(membersRaw);
|
let members = JSON.parse(membersRaw);
|
||||||
|
|
||||||
let descHtml = `<blah>desc.key.rotation.info</blah><br><br><b>${members.length} <blah>label.members</blah></b>`;
|
let descHtml = `<blah>desc.key.rotation.you</blah><br><br><b>${members.length} <blah>label.keys.to.encrypt</blah></b>`;
|
||||||
|
|
||||||
showConfirmPopup(
|
showConfirmPopup(
|
||||||
"title.key.rotation",
|
"title.key.rotation",
|
||||||
|
|
@ -557,6 +560,7 @@ 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 {
|
||||||
|
|
@ -613,29 +617,12 @@ 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");
|
||||||
console.error(e);
|
showBlahNotification(e.message || e);
|
||||||
showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong");
|
pendingRotationMessagePayload = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2141,7 +2128,7 @@ async function renderInvites(res, type) {
|
||||||
let actions = "";
|
let actions = "";
|
||||||
let desc = "";
|
let desc = "";
|
||||||
|
|
||||||
let displayName = username.split(':')[0] || onlyId;
|
let displayName = username || onlyId;
|
||||||
if (type === "received") {
|
if (type === "received") {
|
||||||
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
desc = `:desc.invite.dm.received:${displayName};${onlyId}`;
|
||||||
actions = `
|
actions = `
|
||||||
|
|
@ -2195,7 +2182,7 @@ async function renderInvites(res, type) {
|
||||||
let actions = "";
|
let actions = "";
|
||||||
let desc = "";
|
let desc = "";
|
||||||
|
|
||||||
let displayName = username.split(':')[0] || onlyId;
|
let displayName = username || 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>`;
|
||||||
|
|
||||||
|
|
@ -2399,6 +2386,13 @@ 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) {
|
||||||
|
|
@ -2591,6 +2585,8 @@ 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">
|
||||||
|
|
@ -2598,6 +2594,7 @@ 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++;
|
||||||
|
|
@ -2651,6 +2648,8 @@ 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">
|
||||||
|
|
@ -2658,6 +2657,7 @@ 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",
|
||||||
"",
|
"",
|
||||||
"prompt.invite.username",
|
"title.username",
|
||||||
"action.invite",
|
"action.invite",
|
||||||
"action.cancel",
|
"action.cancel",
|
||||||
async (targetUsername) => {
|
async (targetUsername) => {
|
||||||
|
|
@ -2762,9 +2762,63 @@ 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);
|
||||||
|
|
@ -2857,10 +2911,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; 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;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);">`;
|
||||||
} else {
|
} else {
|
||||||
let pfp = await getGroupAvatarUrl(dmId, username);
|
let pfp = await getGroupAvatarUrl(dmId, username);
|
||||||
iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
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);">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRoomsBarTitle !== "title.home") {
|
if (currentRoomsBarTitle !== "title.home") {
|
||||||
|
|
@ -3018,8 +3072,27 @@ 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.startsWith("image/") || att.type.startsWith("video/");
|
let isMedia = att.type && (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
|
||||||
|
|
@ -3060,7 +3133,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") {
|
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") {
|
||||||
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]) {
|
||||||
|
|
@ -3083,6 +3156,7 @@ 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>`;
|
||||||
|
|
||||||
|
|
@ -3090,6 +3164,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -3521,7 +3597,10 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name,
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading</div>`;
|
if (window.currentRoomType === 'groups' && window.groupKeysCache[currentDmId]) {
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3648,12 +3727,17 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decText && decText.startsWith("success:")) {
|
if (decText && decText.startsWith("success:")) {
|
||||||
attachmentsPayload.push({
|
let metadataObj = {
|
||||||
id: i,
|
|
||||||
type: file.type,
|
type: file.type,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size
|
||||||
staging_id: decText.split(":")[1]
|
};
|
||||||
|
let encMetadata = "ENC:" + await encryptAesGcmToBase64(JSON.stringify(metadataObj), dmKeyBytes);
|
||||||
|
|
||||||
|
attachmentsPayload.push({
|
||||||
|
id: i,
|
||||||
|
staging_id: decText.split(":")[1],
|
||||||
|
enc: encMetadata
|
||||||
});
|
});
|
||||||
} 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");
|
||||||
|
|
@ -3671,9 +3755,11 @@ 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';
|
||||||
|
|
@ -3693,7 +3779,13 @@ 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);
|
||||||
|
|
@ -4282,7 +4374,12 @@ async function changePassword() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAccount() {
|
async function deleteAccount() {
|
||||||
if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return;
|
showConfirmPopup(
|
||||||
|
"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");
|
||||||
|
|
@ -4293,6 +4390,10 @@ async function deleteAccount() {
|
||||||
} else {
|
} else {
|
||||||
showBlahNotification(text);
|
showBlahNotification(text);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
|
|
@ -4349,8 +4450,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' ? 512 : 494;
|
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494;
|
||||||
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
|
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200;
|
||||||
let aspectRatio = targetWidth / targetHeight;
|
let aspectRatio = targetWidth / targetHeight;
|
||||||
|
|
||||||
container.style.height = 'auto';
|
container.style.height = 'auto';
|
||||||
|
|
@ -4418,8 +4519,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' ? 512 : 988;
|
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988;
|
||||||
let targetHeight = cropperType === 'avatar' ? 512 : 400;
|
let targetHeight = cropperType === 'avatar' || cropperType === 'group-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;
|
||||||
|
|
@ -4590,10 +4691,20 @@ async function confirmCropper() {
|
||||||
newSrc = 'data:image/webp;base64,' + b64;
|
newSrc = 'data:image/webp;base64,' + b64;
|
||||||
}
|
}
|
||||||
|
|
||||||
let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
|
let payloadKey = (cropperType === 'avatar' || cropperType === 'group-avatar') ? 'larp.profile.pfp' : 'larp.profile.banner';
|
||||||
let fileBytes = base64ToUint8(b64);
|
|
||||||
showAction("action.uploading", "uploadmedia");
|
showAction("action.uploading", "uploadmedia");
|
||||||
let res = await fetchEncryptedUpload(payloadKey, fileBytes);
|
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);
|
||||||
|
res = await fetchEncryptedUpload(payloadKey, fileBytes);
|
||||||
|
}
|
||||||
clearAction("uploadmedia");
|
clearAction("uploadmedia");
|
||||||
if (res && res.startsWith("error:")) {
|
if (res && res.startsWith("error:")) {
|
||||||
showBlahNotification(res);
|
showBlahNotification(res);
|
||||||
|
|
@ -4603,9 +4714,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4673,6 +4790,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -4705,6 +4827,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -4927,9 +5054,48 @@ function handleGroupContextMenu(e, groupId) {
|
||||||
fixedContextMenu.style.top = `${newTop}px`;
|
fixedContextMenu.style.top = `${newTop}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoGroupSettings(groupId) {
|
async 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) {
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,18 @@ 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>
|
||||||
<div class="input-group">
|
<label class="toggle-switch">
|
||||||
|
<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>
|
||||||
|
|
@ -211,7 +221,7 @@ var profileContextMenu = `
|
||||||
<span class="blah">title.profile</span>
|
<span class="blah">title.profile</span>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="gotoSettings()">
|
<button onclick="gotoSettings()">
|
||||||
${ICONS.settingsProfile()}
|
${ICONS.settings()}
|
||||||
<span class="blah">title.settings</span>
|
<span class="blah">title.settings</span>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="logout()">
|
<button onclick="logout()">
|
||||||
|
|
@ -327,7 +337,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 blah" onclick="deleteAccount()" style="background: var(--big-red); color: white;">action.delete.account</button>
|
<button class="submit-button popup-btn-danger blah" onclick="deleteAccount()">action.delete.account</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -368,6 +378,36 @@ 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;">
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,7 @@ 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;
|
||||||
|
|
@ -878,35 +879,27 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
.popup-btn-cancel {
|
button.profile-action-btn.popup-action-btn {
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
.popup-btn-cancel:hover {
|
button.profile-action-btn.popup-btn-cancel:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
.popup-btn-confirm {
|
button.profile-action-btn.popup-btn-danger, button.submit-button.popup-btn-danger {
|
||||||
background: var(--text-color);
|
background: var(--big-red) !important;
|
||||||
color: var(--main-bg-color);
|
color: white !important;
|
||||||
border: none;
|
border: none;
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.popup-btn-confirm:hover {
|
button.profile-action-btn.popup-btn-danger:hover, button.submit-button.popup-btn-danger:hover {
|
||||||
background: white;
|
filter: brightness(0.85);
|
||||||
}
|
|
||||||
.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);
|
||||||
|
|
@ -1072,3 +1065,106 @@ 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); }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue