`;
for (let att of atts) {
if (att.enc && att.enc.startsWith("ENC:")) {
try {
@@ -3116,7 +3163,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
console.error("Failed to parse attachments", e);
}
} else {
- attachmentsHtml += `
+ attachmentsHtml += `
[${processBlah("attachment.legacy")}: ${escapeHtml(msg.attachment)}]
`;
}
@@ -3133,7 +3180,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") {
+ 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") {
let authorName = processBlah("title.unknown");
if (msg.author && msg.author !== "0") {
if (!dmUsernameCache[msg.author]) {
@@ -3157,6 +3204,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
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";
+ 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";
let clickableAuthor = `
${authorName}`;
@@ -3166,6 +3215,18 @@ async function generateMessagesHtml(entries, isPinnedContext = false) {
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.kick" || msg.type === "larp.info.transfer_owner") {
+ let targetUsername = "";
+ let targetId = "";
+ let parts = content.split(':');
+ if (parts.length >= 2 && parts[0].includes(';')) {
+ targetId = parts[0];
+ targetUsername = escapeHtml(parts.slice(1).join(':'));
+ } else {
+ targetUsername = escapeHtml(parts[0]);
+ }
+ let clickableTarget = targetId ? `
${targetUsername}` : `
${targetUsername}`;
+ content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableTarget);
} else {
content = textStr.replace("{0}", clickableAuthor);
}
@@ -3424,6 +3485,20 @@ async function renderMessages(messages, isPrepend = false) {
oldReactions.remove();
}
+ let oldAttachments = existingChild.querySelector('.chat-message-attachments');
+ let newAttachments = newChild.querySelector('.chat-message-attachments');
+ if (newAttachments) {
+ if (oldAttachments) {
+ if (oldAttachments.getAttribute('data-raw') !== newAttachments.getAttribute('data-raw')) oldAttachments.innerHTML = newAttachments.innerHTML;
+ } else {
+ let contentNode = existingChild.querySelector('.chat-message-content');
+ if (oldReactions) contentNode.insertBefore(newAttachments, oldReactions);
+ else contentNode.appendChild(newAttachments);
+ }
+ } else if (oldAttachments) {
+ oldAttachments.remove();
+ }
+
let oldPfp = existingChild.querySelector('.chat-message-pfp');
let newPfp = newChild.querySelector('.chat-message-pfp');
if (oldPfp && newPfp && oldPfp.src !== newPfp.src) {
@@ -3920,7 +3995,6 @@ let touchContextMenuFired = false;
function handleMessageContextMenu(e, msgId) {
e.preventDefault();
if (touchContextMenuFired) {
- touchContextMenuFired = false;
return;
}
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
@@ -5013,6 +5087,96 @@ if (typeof roomTopBar !== 'undefined' && roomTopBar) {
var currentContextMenuGroupId = null;
+function handleMemberContextMenu(e, groupId, memberId) {
+ if (window.currentRoomType !== 'groups') return;
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (e && e.type === "contextmenu") {
+ touchContextMenuFired = true;
+ setTimeout(() => { touchContextMenuFired = false; }, 500);
+ }
+
+ let html = '';
+
+ let myIdStr = window.id + ";" + window.host;
+ if (window.currentGroupOwner === myIdStr && memberId !== myIdStr) {
+ html += `
+
+
+ `;
+ }
+
+ if (html !== '') {
+ let clientX = e.clientX !== undefined ? e.clientX : (e.touches && e.touches.length > 0 ? e.touches[0].clientX : 0);
+ let clientY = e.clientY !== undefined ? e.clientY : (e.touches && e.touches.length > 0 ? e.touches[0].clientY : 0);
+ showFixedContextMenu({top: clientY, right: clientX, bottom: clientY, left: clientX}, html);
+ }
+}
+
+async function kickFromGroup(groupId, memberId) {
+ if (fixedContextMenu.classList.contains("show")) {
+ fixedContextMenu.classList.remove("show");
+ }
+
+ showAction("action.processing", "kick.action");
+ let payload = JSON.stringify({
+ string1: groupId,
+ string2: memberId
+ });
+
+ let res = await fetchEncrypted("group/kick", payload);
+ clearAction("kick.action");
+
+ if (res && res.startsWith("success")) {
+ showBlahNotification("success:kicked");
+ showGroupMembers(groupId).then(html => {
+ let container = document.getElementById("right-sidebar-container");
+ if (container) {
+ container.innerHTML = html;
+ translate(container);
+ }
+ });
+ } else {
+ showBlahNotification(res || "error:something.wrong");
+ }
+}
+
+async function transferGroupOwnership(groupId, memberId) {
+ if (fixedContextMenu.classList.contains("show")) {
+ fixedContextMenu.classList.remove("show");
+ }
+
+ showAction("action.processing", "transfer.action");
+ let payload = JSON.stringify({
+ string1: groupId,
+ string2: memberId
+ });
+
+ let res = await fetchEncrypted("group/transferowner", payload);
+ clearAction("transfer.action");
+
+ if (res && res.startsWith("success")) {
+ window.currentGroupOwner = memberId;
+ showBlahNotification("success:owner.transferred");
+ showGroupMembers(groupId).then(html => {
+ let container = document.getElementById("right-sidebar-container");
+ if (container) {
+ container.innerHTML = html;
+ translate(container);
+ }
+ });
+ } else {
+ showBlahNotification(res || "error:something.wrong");
+ }
+}
+
function handleGroupContextMenu(e, groupId) {
if (e) {
e.preventDefault();
@@ -5020,6 +5184,10 @@ function handleGroupContextMenu(e, groupId) {
}
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
currentContextMenuGroupId = groupId;
+ if (e && e.type === "contextmenu") {
+ touchContextMenuFired = true;
+ setTimeout(() => { touchContextMenuFired = false; }, 500);
+ }
let btn = e ? (e.currentTarget || e.target) : null;
let rect;
@@ -5062,6 +5230,8 @@ async function gotoGroupSettings(groupId) {
let infoStr = await fetchEncrypted("group/info", groupId);
if (!infoStr.startsWith("error:")) {
let info = JSON.parse(infoStr);
+ window.currentGroupOwner = info.string7;
+
await switchRoomContent("title.group.settings", groupSettingsScreen, false);
document.getElementById('group-settings-name').value = info.string2;
@@ -5070,6 +5240,19 @@ async function gotoGroupSettings(groupId) {
document.getElementById('group-settings-avatar-preview').src = pfpUrl;
document.getElementById('group-settings-history').checked = (info.string6 === "true");
+
+ let myIdStr = window.id + ";" + window.host;
+ let historyCheckbox = document.getElementById('group-settings-history');
+ let historyContainer = historyCheckbox.closest('.setting-row');
+ if (historyContainer) {
+ if (window.currentGroupOwner !== myIdStr) {
+ historyContainer.style.display = 'none';
+ } else {
+ historyContainer.style.display = 'flex';
+ }
+ } else {
+ historyCheckbox.disabled = (window.currentGroupOwner !== myIdStr);
+ }
} else {
showBlahNotification(infoStr);
}
@@ -5120,7 +5303,13 @@ async function confirmLeaveGroup(groupId) {
let res = await fetchEncrypted("group/leave", payload);
if (res === "success") {
if (currentDmId === groupId) {
- gotoHome();
+ currentDmId = null;
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
+ if (window.innerWidth <= 55 * rem) {
+ mobileNavBack();
+ } else {
+ gotoHome();
+ }
}
refreshGroups(0, false);
} else if (res.startsWith("error:")) {
diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json
index a5e5a89f..ae56e047 100644
--- a/webroot/blah/en-cat.json
+++ b/webroot/blah/en-cat.json
@@ -267,5 +267,28 @@
"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"
+ "title.leave.group": "Leave group, meow",
+ "role.member": "Meowber",
+ "role.group_owner": "Big purr",
+ "role.Owner": "Big purr",
+ "action.kick": "Hiss and scratch from group",
+ "action.transfer_owner": "Give shiny toy",
+ "group.user.kicked.by": "{0} hissed at {1} and they ran away",
+ "group.owner.transferred.by": "{0} gave the shiny toy to {1}",
+ "group.user.kicked": "Kitty kicked",
+ "group.owner.transferred": "Shiny toy given",
+ "success:kicked": "Hissed away successfully",
+ "success:owner.transferred": "Toy transferred",
+ "error:cannot.kick.self": "You can't scratch yourself",
+ "action.saving": "Meowving...",
+ "desc.no.chat.selected": "No purr selected. Select a purr from the sidebar to start meowing.",
+ "failed.to.prepare.keys": "Failed to prepare meows",
+ "group.name.empty": "Purr name cannot be empty",
+ "group.refresh.failed": "Failed to refresh purr",
+ "group.settings.saved": "Purr settings saved successfully",
+ "kicked": "Bited successfully",
+ "message.delete.failed": "Failed to delete meow",
+ "message.react.failed": "Failed to react to meow",
+ "message.send.failed": "Failed to send meow",
+ "owner.transferred": "Purrship transferred successfully"
}
\ No newline at end of file
diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json
index d9ccd071..15e60677 100644
--- a/webroot/blah/en-us.json
+++ b/webroot/blah/en-us.json
@@ -249,7 +249,7 @@
"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.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)",
@@ -273,6 +273,28 @@
"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"
+ "title.leave.group": "Leave group",
+ "role.member": "Member",
+ "role.group_owner": "Group owner",
+ "role.Owner": "Owner",
+ "action.kick": "Kick from group",
+ "action.transfer_owner": "Transfer ownership",
+ "group.user.kicked.by": "{0} kicked {1} from the group",
+ "group.owner.transferred.by": "{0} transferred ownership to {1}",
+ "group.user.kicked": "User kicked",
+ "group.owner.transferred": "Ownership transferred",
+ "success:kicked": "User kicked successfully",
+ "success:owner.transferred": "Ownership transferred",
+ "error:cannot.kick.self": "You cannot kick yourself",
+ "action.saving": "Saving...",
+ "desc.no.chat.selected": "No chat selected. Select a chat from the sidebar to start messaging.",
+ "failed.to.prepare.keys": "Failed to prepare keys",
+ "group.name.empty": "Group name cannot be empty",
+ "group.refresh.failed": "Failed to refresh group",
+ "group.settings.saved": "Group settings saved successfully",
+ "kicked": "Kicked successfully",
+ "message.delete.failed": "Failed to delete message",
+ "message.react.failed": "Failed to react to message",
+ "message.send.failed": "Failed to send message",
+ "owner.transferred": "Ownership transferred successfully"
}
\ No newline at end of file
diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json
index 1476621b..1afbc3b1 100644
--- a/webroot/blah/pl-pl.json
+++ b/webroot/blah/pl-pl.json
@@ -273,6 +273,28 @@
"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ę"
+ "title.leave.group": "Opuść grupę",
+ "role.member": "Członek",
+ "role.group_owner": "Właściciel grupy",
+ "role.Owner": "Właściciel",
+ "action.kick": "Wyrzuć z grupy",
+ "action.transfer_owner": "Przekaż właściciela",
+ "group.user.kicked.by": "{0} wyrzucił/a {1} z grupy",
+ "group.owner.transferred.by": "{0} przekazał/a właściciela dla {1}",
+ "group.user.kicked": "Użytkownik wyrzucony",
+ "group.owner.transferred": "Właściciel przekazany",
+ "success:kicked": "Użytkownik wyrzucony pomyślnie",
+ "success:owner.transferred": "Właściciel przekazany pomyślnie",
+ "error:cannot.kick.self": "Nie możesz wyrzucić samego siebie",
+ "action.saving": "Zapisywanie...",
+ "desc.no.chat.selected": "Nie wybrano czatu. Wybierz czat z paska bocznego, aby rozpocząć pisanie.",
+ "failed.to.prepare.keys": "Nie udało się przygotować kluczy",
+ "group.name.empty": "Nazwa grupy nie może być pusta",
+ "group.refresh.failed": "Nie udało się odświeżyć grupy",
+ "group.settings.saved": "Ustawienia grupy zapisane pomyślnie",
+ "kicked": "Wyrzucono pomyślnie",
+ "message.delete.failed": "Nie udało się usunąć wiadomości",
+ "message.react.failed": "Nie udało się dodać reakcji",
+ "message.send.failed": "Nie udało się wysłać wiadomości",
+ "owner.transferred": "Własność przekazana pomyślnie"
}
\ No newline at end of file
diff --git a/webroot/main.js b/webroot/main.js
index 91fa2d52..8170cb93 100644
--- a/webroot/main.js
+++ b/webroot/main.js
@@ -1563,13 +1563,19 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
roomsTopBarTransition.style.opacity = "0";
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
+ let slideIn = false;
if (window.innerWidth <= 55 * rem && !skipMobileSlide) {
if (title != "title.splash") //do not show splash on mobile
{
+ if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
+ slideIn = true;
+ }
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
}
- } else {
+ }
+
+ if (!slideIn) {
await delay(200);
}
@@ -1762,6 +1768,7 @@ function clearContextMenuStyles() {
}
document.addEventListener("click", (e) => {
+ if (touchContextMenuFired) return;
history.pushState({trap: true}, "", location.href);
if (fixedContextMenu.classList.contains("show")) {
@@ -1778,7 +1785,11 @@ document.addEventListener("click", (e) => {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth <= 55 * rem) {
if (!roomDetailsBar.contains(e.target) && !e.target.closest('#room-topbar-clickable') && !e.target.closest('.profile-popup-container') && !e.target.closest('.room-action-pin')) {
- mobileNavBack();
+ if (currentRoomContentTitle === "title.group.settings") {
+ openDm(window.currentSettingsGroupId, document.getElementById('group-settings-name').value, window.currentSettingsGroupId, 'groups');
+ } else {
+ mobileNavBack();
+ }
}
}
}
@@ -1848,6 +1859,7 @@ function logout() {
let isProfileOpen = false;
async function openProfile(accountId) {
+ if (touchContextMenuFired) return;
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
@@ -1969,6 +1981,19 @@ async function openProfile(accountId) {
}
}
}
+ let roleHtml = "";
+ if (window.currentDmId && window.currentRoomType && (window.currentRoomType === "groups" || window.currentRoomType === "dms" || window.currentRoomType === "spaces")) {
+ let payload = JSON.stringify({
+ string1: window.currentRoomType,
+ string2: window.currentDmId,
+ string3: accountId
+ });
+ let roleRes = await fetchEncrypted("room/role", payload);
+ if (roleRes && !roleRes.startsWith("error:")) {
+ let mappedRole = "role." + roleRes.replace(" ", "_");
+ roleHtml = `
${mappedRole}
`;
+ }
+ }
popupInner.innerHTML = `
${bannerHtml}
@@ -1979,6 +2004,7 @@ async function openProfile(accountId) {
${profileName}
${profileName}:${profileHost} (${accountId.split(':')[0].split(';')[0]})
+ ${roleHtml}
${bioHtml}
${actionBtnHtml}
@@ -2423,6 +2449,11 @@ function setActiveSidebarIndicator(element, isTemporary = false) {
}
function mobileNavBack() {
+ if (currentRoomContentTitle === "title.group.settings") {
+ openDm(window.currentSettingsGroupId, document.getElementById('group-settings-name').value, window.currentSettingsGroupId, 'groups');
+ return;
+ }
+
if (mainScreen.classList.contains('mobile-details')) {
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
@@ -2512,6 +2543,7 @@ document.addEventListener('touchend', async (e) => {
}
let elapsed = Date.now() - pressDate; //najgorszy fix swiata
+
if (elapsed < 100) {
const remaining = 100 - elapsed;
await delay(remaining);
@@ -2861,7 +2893,7 @@ async function generateGroupMembersHtml(groupId, groupName) {
let memberDisplayName = memberUsername.split(':')[0];
html += `
-