From bb135bed65e216963fca67c2c0e0df102c154ebd Mon Sep 17 00:00:00 2001 From: olcxja Date: Sun, 9 Aug 2026 13:21:57 +0200 Subject: [PATCH 1/3] slightly improve popups --- .../src/main/assets/public/blah/en-cat.json | 7 ++- .../src/main/assets/public/blah/en-us.json | 7 ++- .../src/main/assets/public/blah/pl-pl.json | 7 ++- android/app/src/main/assets/public/icons.js | 3 +- android/app/src/main/assets/public/main.js | 55 ++++++++++++------- android/app/src/main/assets/public/screens.js | 4 +- android/app/src/main/assets/public/style.css | 34 +++++------- webroot/blah/en-cat.json | 7 ++- webroot/blah/en-us.json | 7 ++- webroot/blah/pl-pl.json | 7 ++- webroot/icons.js | 3 +- webroot/main.js | 55 ++++++++++++------- webroot/screens.js | 4 +- webroot/style.css | 34 +++++------- 14 files changed, 136 insertions(+), 98 deletions(-) diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index f06e9ea5..a5e5a89f 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -262,5 +262,10 @@ "action.leaving.group": "Leaving group, meow...", "action.key.rotating": "Rotating keys, 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" } \ No newline at end of file diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index effa6346..634760ad 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -262,5 +262,10 @@ "action.leaving.group": "Leaving group...", "action.key.rotating": "Rotating keys...", "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" } \ No newline at end of file diff --git a/android/app/src/main/assets/public/blah/pl-pl.json b/android/app/src/main/assets/public/blah/pl-pl.json index 3a00a975..6b7c54b4 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -262,5 +262,10 @@ "action.leaving.group": "Opuszczanie grupy...", "action.key.rotating": "Obracanie 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ę" } \ No newline at end of file diff --git a/android/app/src/main/assets/public/icons.js b/android/app/src/main/assets/public/icons.js index 06596908..ae3b562c 100644 --- a/android/app/src/main/assets/public/icons.js +++ b/android/app/src/main/assets/public/icons.js @@ -30,11 +30,10 @@ const ICONS = { pin: (props) => ICONS._material(``, props), call: (props) => ICONS._material(``, props), account: (props) => ICONS._material(``, props), - settings: (props) => ICONS._material(``, props), profile: (props) => ICONS._material(``, props), replyAction: (props) => ICONS._material(``, props), react: (props) => ICONS._material(``, props), - settingsProfile: (props) => ICONS._material(``, props), + settings: (props) => ICONS._material(``, props), download: (props) => ICONS._feather(``, props), camera: (props) => ICONS._feather(``, props), diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 7a90b060..4d906816 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -432,7 +432,7 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey, let popupContainer = document.getElementById("generic-popup-container"); let popupInner = document.getElementById("generic-popup-inner"); - let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm"; + let confirmClass = isDestructive ? "popup-btn-danger" : ""; genericPopupCancelCallback = onCancel; @@ -449,10 +449,10 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey, ${descHtml}
- -
@@ -474,7 +474,7 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c let popupContainer = document.getElementById("generic-popup-container"); let popupInner = document.getElementById("generic-popup-inner"); - let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm"; + let confirmClass = isDestructive ? "popup-btn-danger" : ""; genericPopupCancelCallback = onCancel; @@ -491,12 +491,15 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c
${descHtml}
- +
+ + +
- -
@@ -531,7 +534,7 @@ function closeRotationPopup() { async function showRotationPopup(groupId, originalMessagePayload) { pendingRotationMessagePayload = originalMessagePayload; - showAction("action.key.rotation", "key_rotation"); + showAction("action.key.rotating", "key_rotation"); try { let membersRaw = await fetchEncrypted("group/members", groupId); @@ -539,7 +542,7 @@ async function showRotationPopup(groupId, originalMessagePayload) { let members = JSON.parse(membersRaw); - let descHtml = `desc.key.rotation.info

${members.length} label.members`; + let descHtml = `desc.key.rotation.you

${members.length} label.keys.to.encrypt`; showConfirmPopup( "title.key.rotation", @@ -557,6 +560,7 @@ async function showRotationPopup(groupId, originalMessagePayload) { async function executeKeyRotation() { closeRotationPopup(); + clearAction("key_rotation"); showAction("action.key.rotating", "key_rotation_exec"); try { @@ -2742,7 +2746,7 @@ async function inviteToGroupPrompt(groupId) { showPromptPopup( "action.invite", "", - "prompt.invite.username", + "title.username", "action.invite", "action.cancel", async (targetUsername) => { @@ -4282,17 +4286,26 @@ async function changePassword() { } async function deleteAccount() { - if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return; - showAction("action.account.deleting", "accountdel"); - let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); - clearAction("accountdel"); - let text = res; - if (text.startsWith("success:")) { - alert(processBlah(text)); - logout(); - } else { - showBlahNotification(text); - } + showConfirmPopup( + "title.delete.account", + "desc.delete.account

prompt.are.you.sure", + "action.delete.account", + "action.cancel", + async () => { + showAction("action.account.deleting", "accountdel"); + let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); + clearAction("accountdel"); + let text = res; + if (text.startsWith("success:")) { + alert(processBlah(text)); + logout(); + } else { + showBlahNotification(text); + } + }, + null, + true + ); } async function saveProfile() { diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index 8f1f7e04..ddc0a5e2 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -211,7 +211,7 @@ var profileContextMenu = ` title.profile + `; diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index d9c8260e..81f402e4 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -878,35 +878,27 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { height: auto; 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); color: var(--text-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); } -.popup-btn-confirm { - background: var(--text-color); - color: var(--main-bg-color); +button.profile-action-btn.popup-btn-danger, button.submit-button.popup-btn-danger { + background: var(--big-red) !important; + color: white !important; border: none; - flex: 1; - justify-content: center; } -.popup-btn-confirm:hover { - background: white; -} -.popup-btn-danger { - background: var(--big-red); - color: white; - border: none; - flex: 1; - justify-content: center; -} -.popup-btn-danger:hover { - background: #ff5252; +button.profile-action-btn.popup-btn-danger:hover, button.submit-button.popup-btn-danger:hover { + filter: brightness(0.85); } .profile-popup-container.show .profile-popup-content { transform: scale(1); diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index f06e9ea5..a5e5a89f 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -262,5 +262,10 @@ "action.leaving.group": "Leaving group, meow...", "action.key.rotating": "Rotating keys, 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" } \ No newline at end of file diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index effa6346..634760ad 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -262,5 +262,10 @@ "action.leaving.group": "Leaving group...", "action.key.rotating": "Rotating keys...", "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" } \ No newline at end of file diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json index 3a00a975..6b7c54b4 100644 --- a/webroot/blah/pl-pl.json +++ b/webroot/blah/pl-pl.json @@ -262,5 +262,10 @@ "action.leaving.group": "Opuszczanie grupy...", "action.key.rotating": "Obracanie 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ę" } \ No newline at end of file diff --git a/webroot/icons.js b/webroot/icons.js index 06596908..ae3b562c 100644 --- a/webroot/icons.js +++ b/webroot/icons.js @@ -30,11 +30,10 @@ const ICONS = { pin: (props) => ICONS._material(``, props), call: (props) => ICONS._material(``, props), account: (props) => ICONS._material(``, props), - settings: (props) => ICONS._material(``, props), profile: (props) => ICONS._material(``, props), replyAction: (props) => ICONS._material(``, props), react: (props) => ICONS._material(``, props), - settingsProfile: (props) => ICONS._material(``, props), + settings: (props) => ICONS._material(``, props), download: (props) => ICONS._feather(``, props), camera: (props) => ICONS._feather(``, props), diff --git a/webroot/main.js b/webroot/main.js index 7a90b060..4d906816 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -432,7 +432,7 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey, let popupContainer = document.getElementById("generic-popup-container"); let popupInner = document.getElementById("generic-popup-inner"); - let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm"; + let confirmClass = isDestructive ? "popup-btn-danger" : ""; genericPopupCancelCallback = onCancel; @@ -449,10 +449,10 @@ function showConfirmPopup(titleKey, descHtml, confirmActionKey, cancelActionKey, ${descHtml}
- -
@@ -474,7 +474,7 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c let popupContainer = document.getElementById("generic-popup-container"); let popupInner = document.getElementById("generic-popup-inner"); - let confirmClass = isDestructive ? "popup-btn-danger" : "popup-btn-confirm"; + let confirmClass = isDestructive ? "popup-btn-danger" : ""; genericPopupCancelCallback = onCancel; @@ -491,12 +491,15 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c
${descHtml}
- +
+ + +
- -
@@ -531,7 +534,7 @@ function closeRotationPopup() { async function showRotationPopup(groupId, originalMessagePayload) { pendingRotationMessagePayload = originalMessagePayload; - showAction("action.key.rotation", "key_rotation"); + showAction("action.key.rotating", "key_rotation"); try { let membersRaw = await fetchEncrypted("group/members", groupId); @@ -539,7 +542,7 @@ async function showRotationPopup(groupId, originalMessagePayload) { let members = JSON.parse(membersRaw); - let descHtml = `desc.key.rotation.info

${members.length} label.members`; + let descHtml = `desc.key.rotation.you

${members.length} label.keys.to.encrypt`; showConfirmPopup( "title.key.rotation", @@ -557,6 +560,7 @@ async function showRotationPopup(groupId, originalMessagePayload) { async function executeKeyRotation() { closeRotationPopup(); + clearAction("key_rotation"); showAction("action.key.rotating", "key_rotation_exec"); try { @@ -2742,7 +2746,7 @@ async function inviteToGroupPrompt(groupId) { showPromptPopup( "action.invite", "", - "prompt.invite.username", + "title.username", "action.invite", "action.cancel", async (targetUsername) => { @@ -4282,17 +4286,26 @@ async function changePassword() { } async function deleteAccount() { - if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return; - showAction("action.account.deleting", "accountdel"); - let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); - clearAction("accountdel"); - let text = res; - if (text.startsWith("success:")) { - alert(processBlah(text)); - logout(); - } else { - showBlahNotification(text); - } + showConfirmPopup( + "title.delete.account", + "desc.delete.account

prompt.are.you.sure", + "action.delete.account", + "action.cancel", + async () => { + showAction("action.account.deleting", "accountdel"); + let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); + clearAction("accountdel"); + let text = res; + if (text.startsWith("success:")) { + alert(processBlah(text)); + logout(); + } else { + showBlahNotification(text); + } + }, + null, + true + ); } async function saveProfile() { diff --git a/webroot/screens.js b/webroot/screens.js index 8f1f7e04..ddc0a5e2 100644 --- a/webroot/screens.js +++ b/webroot/screens.js @@ -211,7 +211,7 @@ var profileContextMenu = ` title.profile + `; diff --git a/webroot/style.css b/webroot/style.css index d9c8260e..81f402e4 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -878,35 +878,27 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { height: auto; 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); color: var(--text-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); } -.popup-btn-confirm { - background: var(--text-color); - color: var(--main-bg-color); +button.profile-action-btn.popup-btn-danger, button.submit-button.popup-btn-danger { + background: var(--big-red) !important; + color: white !important; border: none; - flex: 1; - justify-content: center; } -.popup-btn-confirm:hover { - background: white; -} -.popup-btn-danger { - background: var(--big-red); - color: white; - border: none; - flex: 1; - justify-content: center; -} -.popup-btn-danger:hover { - background: #ff5252; +button.profile-action-btn.popup-btn-danger:hover, button.submit-button.popup-btn-danger:hover { + filter: brightness(0.85); } .profile-popup-container.show .profile-popup-content { transform: scale(1); From 08ff4755a11c2b17ddefbfc8f3dd782a7c773ba8 Mon Sep 17 00:00:00 2001 From: olcxja Date: Sun, 9 Aug 2026 16:51:37 +0200 Subject: [PATCH 2/3] group settings (TEST) --- .../src/main/assets/public/blah/en-us.json | 7 ++ .../src/main/assets/public/blah/pl-pl.json | 7 ++ android/app/src/main/assets/public/index.html | 5 +- android/app/src/main/assets/public/main.js | 76 ++++++++++++++--- android/app/src/main/assets/public/screens.js | 44 +++++++++- android/app/src/main/assets/public/style.css | 81 +++++++++++++++++++ webroot/blah/en-us.json | 7 ++ webroot/blah/pl-pl.json | 7 ++ webroot/index.html | 5 +- webroot/main.js | 76 ++++++++++++++--- webroot/screens.js | 44 +++++++++- webroot/style.css | 81 +++++++++++++++++++ 12 files changed, 416 insertions(+), 24 deletions(-) diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index 634760ad..d9ccd071 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -249,6 +249,13 @@ "group.user.joined": "Joined group", "prompt.invite.username": "Enter username to invite:", "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?", "title.unknown.group": "Unknown Group", "title.unknown": "Unknown", diff --git a/android/app/src/main/assets/public/blah/pl-pl.json b/android/app/src/main/assets/public/blah/pl-pl.json index 6b7c54b4..1476621b 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -248,6 +248,13 @@ "group.user.joined": "Dołączono do grupy", "prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:", "desc.failed.load.members": "Nie udało się załadować członków", + "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?", "title.unknown.group": "Nieznana grupa", "title.unknown": "Nieznany", diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html index 9156be42..ace3372d 100644 --- a/android/app/src/main/assets/public/index.html +++ b/android/app/src/main/assets/public/index.html @@ -256,9 +256,12 @@ const roomKey = randomRoomKey256Chars(); const selfWrapped = await wrapRoomKeyForSelf(roomKey); + let historyVisible = document.getElementById("creategroup-history") ? document.getElementById("creategroup-history").checked.toString() : "false"; + let payload = JSON.stringify({ string1: groupName, - string2: selfWrapped + string2: selfWrapped, + string3: historyVisible }); let res = await fetchEncrypted("group/create", payload); diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 4d906816..7070e62d 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -492,7 +492,7 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c ${descHtml}
- +
@@ -3064,7 +3064,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { content = processBlah(content); if (msg.type && msg.type.startsWith("larp.info")) { - if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin" || 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"); if (msg.author && msg.author !== "0") { if (!dmUsernameCache[msg.author]) { @@ -3087,6 +3087,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.leave") actionStr = "group.user.left.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 = `${authorName}`; @@ -3094,6 +3095,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) { let clickableMsg = `${processBlah('title.message.lowercase')}`; content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + } else if (msg.type === "larp.info.history") { + content = textStr.replace("{0}", clickableAuthor).replace("{1}", content); } else { content = textStr.replace("{0}", clickableAuthor); } @@ -4603,10 +4606,20 @@ async function confirmCropper() { newSrc = 'data:image/webp;base64,' + b64; } - let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner'; - let fileBytes = base64ToUint8(b64); + let payloadKey = (cropperType === 'avatar' || cropperType === 'group-avatar') ? 'larp.profile.pfp' : 'larp.profile.banner'; 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"); if (res && res.startsWith("error:")) { showBlahNotification(res); @@ -4616,8 +4629,14 @@ async function confirmCropper() { // Update all matching images on the page instantly document.querySelectorAll('img').forEach(img => { - if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) { - img.src = newSrc; + 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'))) { + img.src = newSrc; + } } }); } @@ -4940,9 +4959,48 @@ function handleGroupContextMenu(e, groupId) { fixedContextMenu.style.top = `${newTop}px`; } -function gotoGroupSettings(groupId) { +async function gotoGroupSettings(groupId) { 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) { diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index ddc0a5e2..02580348 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -31,8 +31,18 @@ var createGroupScreen = `
-
-
+ +
+
+ title.group.history.visible + desc.group.history.visible +
+ +
+
@@ -368,6 +378,36 @@ var profileSettings = ` `; +var groupSettingsScreen = ` + +`; var appearanceSettings = `
diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index 81f402e4..23c4e5ac 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -366,6 +366,7 @@ herotitle { gap: 0.2rem; width: 90%; max-width: 30rem; + margin: 0 auto; } .input-group label { font-size: 0.75rem; @@ -1064,3 +1065,83 @@ 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 #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; +} diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index 634760ad..d9ccd071 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -249,6 +249,13 @@ "group.user.joined": "Joined group", "prompt.invite.username": "Enter username to invite:", "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?", "title.unknown.group": "Unknown Group", "title.unknown": "Unknown", diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json index 6b7c54b4..1476621b 100644 --- a/webroot/blah/pl-pl.json +++ b/webroot/blah/pl-pl.json @@ -248,6 +248,13 @@ "group.user.joined": "Dołączono do grupy", "prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:", "desc.failed.load.members": "Nie udało się załadować członków", + "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?", "title.unknown.group": "Nieznana grupa", "title.unknown": "Nieznany", diff --git a/webroot/index.html b/webroot/index.html index 9156be42..ace3372d 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -256,9 +256,12 @@ const roomKey = randomRoomKey256Chars(); const selfWrapped = await wrapRoomKeyForSelf(roomKey); + let historyVisible = document.getElementById("creategroup-history") ? document.getElementById("creategroup-history").checked.toString() : "false"; + let payload = JSON.stringify({ string1: groupName, - string2: selfWrapped + string2: selfWrapped, + string3: historyVisible }); let res = await fetchEncrypted("group/create", payload); diff --git a/webroot/main.js b/webroot/main.js index 4d906816..7070e62d 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -492,7 +492,7 @@ function showPromptPopup(titleKey, descHtml, placeholderKey, confirmActionKey, c ${descHtml}
- +
@@ -3064,7 +3064,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { content = processBlah(content); if (msg.type && msg.type.startsWith("larp.info")) { - if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin" || 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"); if (msg.author && msg.author !== "0") { if (!dmUsernameCache[msg.author]) { @@ -3087,6 +3087,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.leave") actionStr = "group.user.left.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 = `${authorName}`; @@ -3094,6 +3095,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) { let clickableMsg = `${processBlah('title.message.lowercase')}`; content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + } else if (msg.type === "larp.info.history") { + content = textStr.replace("{0}", clickableAuthor).replace("{1}", content); } else { content = textStr.replace("{0}", clickableAuthor); } @@ -4603,10 +4606,20 @@ async function confirmCropper() { newSrc = 'data:image/webp;base64,' + b64; } - let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner'; - let fileBytes = base64ToUint8(b64); + let payloadKey = (cropperType === 'avatar' || cropperType === 'group-avatar') ? 'larp.profile.pfp' : 'larp.profile.banner'; 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"); if (res && res.startsWith("error:")) { showBlahNotification(res); @@ -4616,8 +4629,14 @@ async function confirmCropper() { // Update all matching images on the page instantly document.querySelectorAll('img').forEach(img => { - if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) { - img.src = newSrc; + 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'))) { + img.src = newSrc; + } } }); } @@ -4940,9 +4959,48 @@ function handleGroupContextMenu(e, groupId) { fixedContextMenu.style.top = `${newTop}px`; } -function gotoGroupSettings(groupId) { +async function gotoGroupSettings(groupId) { 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) { diff --git a/webroot/screens.js b/webroot/screens.js index ddc0a5e2..02580348 100644 --- a/webroot/screens.js +++ b/webroot/screens.js @@ -31,8 +31,18 @@ var createGroupScreen = `
-
-
+ +
+
+ title.group.history.visible + desc.group.history.visible +
+ +
+
@@ -368,6 +378,36 @@ var profileSettings = ` `; +var groupSettingsScreen = ` + +`; var appearanceSettings = `
diff --git a/webroot/style.css b/webroot/style.css index 81f402e4..23c4e5ac 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -366,6 +366,7 @@ herotitle { gap: 0.2rem; width: 90%; max-width: 30rem; + margin: 0 auto; } .input-group label { font-size: 0.75rem; @@ -1064,3 +1065,83 @@ 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 #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; +} From 55760ba3686c9ef848546b6c5550bae5dad3bea1 Mon Sep 17 00:00:00 2001 From: olcxja Date: Mon, 10 Aug 2026 02:17:56 +0200 Subject: [PATCH 3/3] bug fixes & ui improvements --- android/app/src/main/assets/public/main.js | 167 +++++++++++++++---- android/app/src/main/assets/public/style.css | 23 +++ webroot/main.js | 167 +++++++++++++++---- webroot/style.css | 23 +++ 4 files changed, 308 insertions(+), 72 deletions(-) diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 7070e62d..3cd1e385 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -617,29 +617,12 @@ async function executeKeyRotation() { clearAction("key_rotation_exec"); showBlahNotification("success:key.rotated"); - // Retry sending the message! - if(pendingRotationMessagePayload) { - let p = pendingRotationMessagePayload; - p.string5 = newIndex; - let dmKeyBytes = await sha256Bytes(base64ToUint8(roomKey)); - p.string2 = await encryptAesGcmToBase64(p.rawPlainContent, dmKeyBytes); // we need to save rawPlainContent for retry - - showAction("action.message.sending", "msgsending"); - let resSend = await fetchEncrypted("group/message/send", JSON.stringify(p)); - clearAction("msgsending"); - - if(resSend && resSend.startsWith("error:")) { - showBlahNotification(resSend); - } else if(resSend && resSend.startsWith("success:")) { - loadDmMessages(currentDmId); - } - } pendingRotationMessagePayload = null; } catch(e) { clearAction("key_rotation_exec"); - console.error(e); - showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong"); + showBlahNotification(e.message || e); + pendingRotationMessagePayload = null; } } @@ -2145,7 +2128,7 @@ async function renderInvites(res, type) { let actions = ""; let desc = ""; - let displayName = username.split(':')[0] || onlyId; + let displayName = username || onlyId; if (type === "received") { desc = `:desc.invite.dm.received:${displayName};${onlyId}`; actions = ` @@ -2199,7 +2182,7 @@ async function renderInvites(res, type) { let actions = ""; let desc = ""; - let displayName = username.split(':')[0] || onlyId; + let displayName = username || onlyId; let val1 = `${displayName}`; let val2 = `${onlyId}`; @@ -2403,6 +2386,13 @@ function gotoJoinSpace() { } async function gotoHome(splash = true) { + if (dmMessagePollInterval) { + clearInterval(dmMessagePollInterval); + dmMessagePollInterval = null; + } + currentDmId = null; + window.currentRoomType = null; + if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar setActiveSidebarIndicator(sidebarHomeIndicator); if (splash) { @@ -2595,6 +2585,8 @@ async function renderDms(res) { let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27"); + let unreadBadge = `
`; + html += `
+ ${unreadBadge} `; count++; @@ -2655,6 +2648,8 @@ async function renderGroups(res) { let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27"); let pfp = await getGroupAvatarUrl(groupId, groupName); + let unreadBadge = `
`; + html += ` `; count++; @@ -2766,9 +2762,63 @@ async function inviteToGroupPrompt(groupId) { try { 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({ string1: groupId, - string2: targetId + string2: targetId, + string3: keysPayload }); let res = await fetchEncrypted("group/invite", payload); @@ -2861,10 +2911,10 @@ async function openDm(dmId, username, targetId, type = 'dms') { let iconHtml = ""; if (type === 'dms') { let pfp = await getAvatarUrl(targetId, username); - iconHtml = ``; + iconHtml = ``; } else { let pfp = await getGroupAvatarUrl(dmId, username); - iconHtml = ``; + iconHtml = ``; } if (currentRoomsBarTitle !== "title.home") { @@ -3022,8 +3072,27 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { if (atts.length > 0) { attachmentsHtml += `
`; 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 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 displayName = truncateFilename(att.name, 25); let iconSvg = isMedia @@ -3528,7 +3597,10 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name, } } catch (e) { console.error(e); - container.innerHTML = `
Error loading
`; + if (window.currentRoomType === 'groups' && window.groupKeysCache[currentDmId]) { + delete window.groupKeysCache[currentDmId][keyIndex]; + } + container.innerHTML = `
Error loading - click to retry
`; container.dataset.loading = "false"; } } @@ -3655,12 +3727,17 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag } if (decText && decText.startsWith("success:")) { - attachmentsPayload.push({ - id: i, + let metadataObj = { type: file.type, name: file.name, - size: file.size, - staging_id: decText.split(":")[1] + size: file.size + }; + let encMetadata = "ENC:" + await encryptAesGcmToBase64(JSON.stringify(metadataObj), dmKeyBytes); + + attachmentsPayload.push({ + id: i, + staging_id: decText.split(":")[1], + enc: encMetadata }); } else { throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed"); @@ -3678,9 +3755,11 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag if (window.currentRoomType === 'groups') { msgPayload.string5 = window.currentGroupKeyIndex || "0"; - msgPayload.rawPlainContent = content; // for retry } + let savedContentForRestore = content; + let savedAttachmentsForRestore = [...currentDraftAttachments]; + input.value = ""; input.style.height = '2.5rem'; sendbutton.style.height = '2.5rem'; @@ -3700,7 +3779,13 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') { let pElem = document.getElementById(pendingMsgId); 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:")) { showBlahNotification(res); let pElem = document.getElementById(pendingMsgId); @@ -4365,8 +4450,8 @@ function initCropper() { let zoomSlider = document.getElementById('cropper-zoom'); // Set aspect ratio - let targetWidth = cropperType === 'avatar' ? 512 : 494; - let targetHeight = cropperType === 'avatar' ? targetWidth : 200; + let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494; + let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200; let aspectRatio = targetWidth / targetHeight; container.style.height = 'auto'; @@ -4434,8 +4519,8 @@ async function confirmCropper() { let zoom = parseFloat(document.getElementById('cropper-zoom').value); let container = document.getElementById('cropper-viewport-container'); - let targetWidth = cropperType === 'avatar' ? 512 : 988; - let targetHeight = cropperType === 'avatar' ? 512 : 400; + let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988; + let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 400; let scaleRatio = targetWidth / container.clientWidth; let drawX = cropperPosX * scaleRatio; let drawY = cropperPosY * scaleRatio; @@ -4705,6 +4790,11 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) { if (nameSpan) { 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) { container.prepend(btn); } @@ -4737,6 +4827,11 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) { if (nameSpan) { 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) { container.prepend(btn); } diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index 23c4e5ac..94ec00ed 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -1145,3 +1145,26 @@ input:checked + .toggle-slider:before { 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); } +} diff --git a/webroot/main.js b/webroot/main.js index 7070e62d..3cd1e385 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -617,29 +617,12 @@ async function executeKeyRotation() { clearAction("key_rotation_exec"); showBlahNotification("success:key.rotated"); - // Retry sending the message! - if(pendingRotationMessagePayload) { - let p = pendingRotationMessagePayload; - p.string5 = newIndex; - let dmKeyBytes = await sha256Bytes(base64ToUint8(roomKey)); - p.string2 = await encryptAesGcmToBase64(p.rawPlainContent, dmKeyBytes); // we need to save rawPlainContent for retry - - showAction("action.message.sending", "msgsending"); - let resSend = await fetchEncrypted("group/message/send", JSON.stringify(p)); - clearAction("msgsending"); - - if(resSend && resSend.startsWith("error:")) { - showBlahNotification(resSend); - } else if(resSend && resSend.startsWith("success:")) { - loadDmMessages(currentDmId); - } - } pendingRotationMessagePayload = null; } catch(e) { clearAction("key_rotation_exec"); - console.error(e); - showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong"); + showBlahNotification(e.message || e); + pendingRotationMessagePayload = null; } } @@ -2145,7 +2128,7 @@ async function renderInvites(res, type) { let actions = ""; let desc = ""; - let displayName = username.split(':')[0] || onlyId; + let displayName = username || onlyId; if (type === "received") { desc = `:desc.invite.dm.received:${displayName};${onlyId}`; actions = ` @@ -2199,7 +2182,7 @@ async function renderInvites(res, type) { let actions = ""; let desc = ""; - let displayName = username.split(':')[0] || onlyId; + let displayName = username || onlyId; let val1 = `${displayName}`; let val2 = `${onlyId}`; @@ -2403,6 +2386,13 @@ function gotoJoinSpace() { } async function gotoHome(splash = true) { + if (dmMessagePollInterval) { + clearInterval(dmMessagePollInterval); + dmMessagePollInterval = null; + } + currentDmId = null; + window.currentRoomType = null; + if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar setActiveSidebarIndicator(sidebarHomeIndicator); if (splash) { @@ -2595,6 +2585,8 @@ async function renderDms(res) { let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27"); + let unreadBadge = `
`; + html += `
+ ${unreadBadge} `; count++; @@ -2655,6 +2648,8 @@ async function renderGroups(res) { let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27"); let pfp = await getGroupAvatarUrl(groupId, groupName); + let unreadBadge = `
`; + html += ` `; count++; @@ -2766,9 +2762,63 @@ async function inviteToGroupPrompt(groupId) { try { 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({ string1: groupId, - string2: targetId + string2: targetId, + string3: keysPayload }); let res = await fetchEncrypted("group/invite", payload); @@ -2861,10 +2911,10 @@ async function openDm(dmId, username, targetId, type = 'dms') { let iconHtml = ""; if (type === 'dms') { let pfp = await getAvatarUrl(targetId, username); - iconHtml = ``; + iconHtml = ``; } else { let pfp = await getGroupAvatarUrl(dmId, username); - iconHtml = ``; + iconHtml = ``; } if (currentRoomsBarTitle !== "title.home") { @@ -3022,8 +3072,27 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { if (atts.length > 0) { attachmentsHtml += `
`; 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 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 displayName = truncateFilename(att.name, 25); let iconSvg = isMedia @@ -3528,7 +3597,10 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name, } } catch (e) { console.error(e); - container.innerHTML = `
Error loading
`; + if (window.currentRoomType === 'groups' && window.groupKeysCache[currentDmId]) { + delete window.groupKeysCache[currentDmId][keyIndex]; + } + container.innerHTML = `
Error loading - click to retry
`; container.dataset.loading = "false"; } } @@ -3655,12 +3727,17 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag } if (decText && decText.startsWith("success:")) { - attachmentsPayload.push({ - id: i, + let metadataObj = { type: file.type, name: file.name, - size: file.size, - staging_id: decText.split(":")[1] + size: file.size + }; + let encMetadata = "ENC:" + await encryptAesGcmToBase64(JSON.stringify(metadataObj), dmKeyBytes); + + attachmentsPayload.push({ + id: i, + staging_id: decText.split(":")[1], + enc: encMetadata }); } else { throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed"); @@ -3678,9 +3755,11 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag if (window.currentRoomType === 'groups') { msgPayload.string5 = window.currentGroupKeyIndex || "0"; - msgPayload.rawPlainContent = content; // for retry } + let savedContentForRestore = content; + let savedAttachmentsForRestore = [...currentDraftAttachments]; + input.value = ""; input.style.height = '2.5rem'; sendbutton.style.height = '2.5rem'; @@ -3700,7 +3779,13 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') { let pElem = document.getElementById(pendingMsgId); 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:")) { showBlahNotification(res); let pElem = document.getElementById(pendingMsgId); @@ -4365,8 +4450,8 @@ function initCropper() { let zoomSlider = document.getElementById('cropper-zoom'); // Set aspect ratio - let targetWidth = cropperType === 'avatar' ? 512 : 494; - let targetHeight = cropperType === 'avatar' ? targetWidth : 200; + let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494; + let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200; let aspectRatio = targetWidth / targetHeight; container.style.height = 'auto'; @@ -4434,8 +4519,8 @@ async function confirmCropper() { let zoom = parseFloat(document.getElementById('cropper-zoom').value); let container = document.getElementById('cropper-viewport-container'); - let targetWidth = cropperType === 'avatar' ? 512 : 988; - let targetHeight = cropperType === 'avatar' ? 512 : 400; + let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988; + let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 400; let scaleRatio = targetWidth / container.clientWidth; let drawX = cropperPosX * scaleRatio; let drawY = cropperPosY * scaleRatio; @@ -4705,6 +4790,11 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) { if (nameSpan) { 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) { container.prepend(btn); } @@ -4737,6 +4827,11 @@ function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) { if (nameSpan) { 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) { container.prepend(btn); } diff --git a/webroot/style.css b/webroot/style.css index 23c4e5ac..94ec00ed 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -1145,3 +1145,26 @@ input:checked + .toggle-slider:before { 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); } +}