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; +}