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 = `
-
-
@@ -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 = `
-
-
@@ -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;
+}