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..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",
@@ -262,5 +269,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..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",
@@ -262,5 +269,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/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 7a90b060..3cd1e385 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 {
@@ -613,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;
}
}
@@ -2141,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 = `
@@ -2195,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}`;
@@ -2399,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) {
@@ -2591,6 +2585,8 @@ async function renderDms(res) {
let safeTargetUsername = encodeURIComponent(targetUsername).replace(/'/g, "%27");
+ let unreadBadge = ``;
+
html += `
`;
count++;
@@ -2651,6 +2648,8 @@ async function renderGroups(res) {
let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27");
let pfp = await getGroupAvatarUrl(groupId, groupName);
+ let unreadBadge = ``;
+
html += `
`;
count++;
@@ -2742,7 +2742,7 @@ async function inviteToGroupPrompt(groupId) {
showPromptPopup(
"action.invite",
"",
- "prompt.invite.username",
+ "title.username",
"action.invite",
"action.cancel",
async (targetUsername) => {
@@ -2762,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);
@@ -2857,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") {
@@ -3018,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
@@ -3060,7 +3133,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]) {
@@ -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.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}`;
@@ -3090,6 +3164,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);
}
@@ -3521,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";
}
}
@@ -3648,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");
@@ -3671,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';
@@ -3693,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);
@@ -4282,17 +4374,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.accountprompt.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() {
@@ -4349,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';
@@ -4418,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;
@@ -4590,10 +4691,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);
@@ -4603,8 +4714,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;
+ }
}
});
}
@@ -4673,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);
}
@@ -4705,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);
}
@@ -4927,9 +5054,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 8f1f7e04..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 = `
-
-
@@ -211,7 +221,7 @@ var profileContextMenu = `
title.profile
- ${ICONS.settingsProfile()}
+ ${ICONS.settings()}
title.settings
@@ -327,7 +337,7 @@ var accountSettings = `
`;
@@ -368,6 +378,36 @@ var profileSettings = `
`;
+var groupSettingsScreen = `
+
+`;
var appearanceSettings = `
diff --git a/webroot/style.css b/webroot/style.css
index d9c8260e..94ec00ed 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;
@@ -878,35 +879,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);
@@ -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 #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); }
+}