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 15e60677..42d98cb7 100644
--- a/android/app/src/main/assets/public/blah/en-us.json
+++ b/android/app/src/main/assets/public/blah/en-us.json
@@ -256,6 +256,9 @@
"history.visible": "enabled",
"history.hidden": "disabled",
"group.history.changed.by": "{0} changed history visibility to: {1}",
+ "group.name.changed.by": "{0} changed the group name to {1}",
+ "group.pfp.changed.by": "{0} changed the group icon",
+ "group.pfp.removed.by": "{0} removed the group icon",
"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 1afbc3b1..d1fea3af 100644
--- a/android/app/src/main/assets/public/blah/pl-pl.json
+++ b/android/app/src/main/assets/public/blah/pl-pl.json
@@ -255,6 +255,9 @@
"history.visible": "włączona",
"history.hidden": "wyłączona",
"group.history.changed.by": "{0} zmienił widoczność historii na: {1}",
+ "group.name.changed.by": "{0} zmienił(a) nazwę grupy na: {1}",
+ "group.pfp.changed.by": "{0} zmienił(a) ikonę grupy",
+ "group.pfp.removed.by": "{0} usunął(ęła) ikonę grupy",
"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 ace3372d..27ac622f 100644
--- a/android/app/src/main/assets/public/index.html
+++ b/android/app/src/main/assets/public/index.html
@@ -109,10 +109,7 @@
diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js
index 6207779b..c00574ed 100644
--- a/android/app/src/main/assets/public/main.js
+++ b/android/app/src/main/assets/public/main.js
@@ -3170,7 +3170,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" || msg.type === "larp.info.history" || msg.type === "larp.info.kick" || msg.type === "larp.info.transfer_owner") {
+ 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" || msg.type === "larp.info.kick" || msg.type === "larp.info.transfer_owner" || msg.type === "larp.info.name" || msg.type === "larp.info.pfp" || msg.type === "larp.info.pfp_removed") {
let authorName = processBlah("title.unknown");
if (msg.author && msg.author !== "0") {
if (!dmUsernameCache[msg.author]) {
@@ -3196,6 +3196,9 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
else if (msg.type === "larp.info.history") actionStr = "group.history.changed.by";
else if (msg.type === "larp.info.kick") actionStr = "group.user.kicked.by";
else if (msg.type === "larp.info.transfer_owner") actionStr = "group.owner.transferred.by";
+ else if (msg.type === "larp.info.name") actionStr = "group.name.changed.by";
+ else if (msg.type === "larp.info.pfp") actionStr = "group.pfp.changed.by";
+ else if (msg.type === "larp.info.pfp_removed") actionStr = "group.pfp.removed.by";
let clickableAuthor = `
${authorName}`;
@@ -3203,8 +3206,9 @@ 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 if (msg.type === "larp.info.history" || msg.type === "larp.info.name") {
+ let escapedContent = escapeHtml(content);
+ content = textStr.replace("{0}", clickableAuthor).replace("{1}", escapedContent);
} else if (msg.type === "larp.info.kick" || msg.type === "larp.info.transfer_owner") {
let targetUsername = "";
let targetId = "";
@@ -4478,6 +4482,7 @@ let cropperImageObj = null;
let cropperPosX = 0;
let cropperPosY = 0;
let cropperBaseZoom = 1;
+let cropperZoom = 1;
let cropperOriginalType = '';
let cropperOriginalB64 = '';
@@ -4509,9 +4514,10 @@ function openCropper(input, type) {
}
function initCropper() {
+ document.getElementById('cropper-popup-container').classList.add('show');
+
let container = document.getElementById('cropper-viewport-container');
let imgEl = document.getElementById('cropper-image');
- let zoomSlider = document.getElementById('cropper-zoom');
// Set aspect ratio
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494;
@@ -4533,16 +4539,12 @@ function initCropper() {
let scaleX = containerWidth / imgWidth;
let scaleY = (containerWidth * (targetHeight / targetWidth)) / imgHeight;
cropperBaseZoom = Math.max(scaleX, scaleY);
-
- zoomSlider.min = cropperBaseZoom;
- zoomSlider.max = cropperBaseZoom * 5;
- zoomSlider.value = cropperBaseZoom;
+ cropperZoom = cropperBaseZoom;
cropperPosX = 0;
cropperPosY = 0;
updateCropperTransform();
- document.getElementById('cropper-popup-container').classList.add('show');
}
function updateCropperZoom() {
@@ -4550,21 +4552,40 @@ function updateCropperZoom() {
}
function updateCropperTransform() {
- let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let imgEl = document.getElementById('cropper-image');
let container = document.getElementById('cropper-viewport-container');
if (cropperImageObj) {
+ let containerWidth = container.clientWidth || 300;
+ let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 494;
+ let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? targetWidth : 200;
+ let containerHeight = container.clientHeight || (containerWidth * (targetHeight / targetWidth));
+
+ let scaleX = containerWidth / cropperImageObj.width;
+ let scaleY = containerHeight / cropperImageObj.height;
+ cropperBaseZoom = Math.max(scaleX, scaleY);
+
+ if (cropperZoom < cropperBaseZoom) cropperZoom = cropperBaseZoom;
+ if (cropperZoom > cropperBaseZoom * 5) cropperZoom = cropperBaseZoom * 5;
+
+ let zoom = cropperZoom;
let scaledW = cropperImageObj.width * zoom;
let scaledH = cropperImageObj.height * zoom;
- let minX = container.clientWidth - scaledW;
+ containerWidth = container.clientWidth || 300;
+ containerHeight = container.clientHeight || (containerWidth * (targetHeight / targetWidth));
+
+ let minX = containerWidth - scaledW;
+ let minY = containerHeight - scaledH;
let maxX = 0;
- let minY = container.clientHeight - scaledH;
let maxY = 0;
- if (minX > maxX) minX = maxX;
- if (minY > maxY) minY = maxY;
+ if (scaledW < containerWidth) {
+ minX = maxX = (containerWidth - scaledW) / 2;
+ }
+ if (scaledH < containerHeight) {
+ minY = maxY = (containerHeight - scaledH) / 2;
+ }
if (cropperPosX < minX) cropperPosX = minX;
if (cropperPosX > maxX) cropperPosX = maxX;
@@ -4572,7 +4593,7 @@ function updateCropperTransform() {
if (cropperPosY > maxY) cropperPosY = maxY;
}
- imgEl.style.transform = `translate(${cropperPosX}px, ${cropperPosY}px) scale(${zoom})`;
+ imgEl.style.transform = `translate(${cropperPosX}px, ${cropperPosY}px) scale(${cropperZoom})`;
}
function closeCropper() {
@@ -4580,16 +4601,16 @@ function closeCropper() {
}
async function confirmCropper() {
- let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let container = document.getElementById('cropper-viewport-container');
let targetWidth = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 988;
let targetHeight = cropperType === 'avatar' || cropperType === 'group-avatar' ? 512 : 400;
- let scaleRatio = targetWidth / container.clientWidth;
+
+ let scaleRatio = targetWidth / (container.clientWidth || 300);
let drawX = cropperPosX * scaleRatio;
let drawY = cropperPosY * scaleRatio;
- let drawW = cropperImageObj.width * zoom * scaleRatio;
- let drawH = cropperImageObj.height * zoom * scaleRatio;
+ let drawW = cropperImageObj.width * cropperZoom * scaleRatio;
+ let drawH = cropperImageObj.height * cropperZoom * scaleRatio;
let b64;
let newSrc;
@@ -4794,9 +4815,20 @@ async function confirmCropper() {
}
-let container = document.getElementById('cropper-viewport-container');
-if (container) {
+let cropperContainer = document.getElementById('cropper-viewport-container');
+if (cropperContainer) {
+ let initialPinchDistance = null;
+ let initialPinchZoom = null;
+
const startDrag = (e) => {
+ if (e.touches && e.touches.length === 2) {
+ isDraggingCropper = false;
+ let dx = e.touches[0].clientX - e.touches[1].clientX;
+ let dy = e.touches[0].clientY - e.touches[1].clientY;
+ initialPinchDistance = Math.sqrt(dx*dx + dy*dy);
+ initialPinchZoom = cropperZoom;
+ return;
+ }
isDraggingCropper = true;
startDragX = e.clientX || (e.touches && e.touches[0].clientX);
startDragY = e.clientY || (e.touches && e.touches[0].clientY);
@@ -4806,6 +4838,30 @@ if (container) {
};
const onDrag = (e) => {
+ if (e.touches && e.touches.length === 2 && initialPinchDistance !== null) {
+ let dx = e.touches[0].clientX - e.touches[1].clientX;
+ let dy = e.touches[0].clientY - e.touches[1].clientY;
+ let distance = Math.sqrt(dx*dx + dy*dy);
+ let zoomFactor = distance / initialPinchDistance;
+
+ let oldZoom = cropperZoom;
+ cropperZoom = initialPinchZoom * zoomFactor;
+
+ if (cropperZoom < cropperBaseZoom) cropperZoom = cropperBaseZoom;
+ if (cropperZoom > cropperBaseZoom * 5) cropperZoom = cropperBaseZoom * 5;
+
+ let zoomRatio = cropperZoom / oldZoom;
+
+ let cw = cropperContainer.clientWidth || 300;
+ let ch = cropperContainer.clientHeight || 300;
+
+ cropperPosX = (cropperPosX - cw/2) * zoomRatio + cw/2;
+ cropperPosY = (cropperPosY - ch/2) * zoomRatio + ch/2;
+
+ updateCropperTransform();
+ return;
+ }
+
if (!isDraggingCropper) return;
let clientX = e.clientX || (e.touches && e.touches[0].clientX);
let clientY = e.clientY || (e.touches && e.touches[0].clientY);
@@ -4814,16 +4870,57 @@ if (container) {
updateCropperTransform();
};
- const endDrag = () => {
- isDraggingCropper = false;
+ const endDrag = (e) => {
+ if (e.touches && e.touches.length < 2) {
+ initialPinchDistance = null;
+ initialPinchZoom = null;
+ }
+ if (e.touches && e.touches.length === 1) {
+ isDraggingCropper = true;
+ startDragX = e.touches[0].clientX;
+ startDragY = e.touches[0].clientY;
+ startPosX = cropperPosX;
+ startPosY = cropperPosY;
+ } else {
+ isDraggingCropper = false;
+ }
+ };
+
+ const onWheel = (e) => {
+ if (document.getElementById('cropper-popup-container').classList.contains('show')) {
+ e.preventDefault();
+ let oldZoom = cropperZoom;
+ let zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
+ cropperZoom *= zoomFactor;
+ if (cropperZoom < cropperBaseZoom) cropperZoom = cropperBaseZoom;
+ if (cropperZoom > cropperBaseZoom * 5) cropperZoom = cropperBaseZoom * 5;
+
+ let zoomRatio = cropperZoom / oldZoom;
+
+ let rect = cropperContainer.getBoundingClientRect();
+ let mouseX = e.clientX - rect.left;
+ let mouseY = e.clientY - rect.top;
+
+ cropperPosX = (cropperPosX - mouseX) * zoomRatio + mouseX;
+ cropperPosY = (cropperPosY - mouseY) * zoomRatio + mouseY;
+
+ updateCropperTransform();
+ }
};
- container.addEventListener('mousedown', startDrag);
- container.addEventListener('touchstart', startDrag, {passive: true});
+ cropperContainer.addEventListener('mousedown', startDrag);
+ cropperContainer.addEventListener('touchstart', startDrag, {passive: true});
document.addEventListener('mousemove', onDrag);
- document.addEventListener('touchmove', onDrag, {passive: true});
+ document.addEventListener('touchmove', onDrag, {passive: false});
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
+ cropperContainer.addEventListener('wheel', onWheel, {passive: false});
+
+ new ResizeObserver(() => {
+ if (document.getElementById('cropper-popup-container').classList.contains('show')) {
+ updateCropperTransform();
+ }
+ }).observe(cropperContainer);
}
function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) {
@@ -5291,7 +5388,7 @@ async function confirmLeaveGroup(groupId) {
try {
let payload = JSON.stringify({ string1: groupId });
let res = await fetchEncrypted("group/leave", payload);
- if (res === "success") {
+ if (res === "success" || res === "error:not.a.member" || res === "error:group.not.found") {
if (currentDmId === groupId) {
currentDmId = null;
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
@@ -5302,6 +5399,9 @@ async function confirmLeaveGroup(groupId) {
}
}
refreshGroups(0, false);
+ if (res !== "success") {
+ showBlahNotification(res);
+ }
} else if (res.startsWith("error:")) {
showBlahNotification(res);
}
diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js
index 2fc8d98c..d0b90b22 100644
--- a/android/app/src/main/assets/public/screens.js
+++ b/android/app/src/main/assets/public/screens.js
@@ -349,7 +349,7 @@ var profileSettings = `