From 55760ba3686c9ef848546b6c5550bae5dad3bea1 Mon Sep 17 00:00:00 2001 From: olcxja Date: Mon, 10 Aug 2026 02:17:56 +0200 Subject: [PATCH] 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 += ` `; 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); } +}