From 3f99389413cc3a6b037278b0fb6a9f7ea75d4b3a Mon Sep 17 00:00:00 2001 From: olcxja Date: Fri, 19 Jun 2026 22:17:33 +0200 Subject: [PATCH] better context menu reposition, add more profile actions --- .../src/main/assets/public/blah/en-cat.json | 8 +- .../src/main/assets/public/blah/en-us.json | 8 +- android/app/src/main/assets/public/main.js | 136 ++++++++++++++---- android/app/src/main/assets/public/screens.js | 4 +- android/app/src/main/assets/public/style.css | 4 +- webroot/blah/en-cat.json | 8 +- webroot/blah/en-us.json | 8 +- webroot/main.js | 136 ++++++++++++++---- webroot/screens.js | 4 +- webroot/style.css | 4 +- 10 files changed, 260 insertions(+), 60 deletions(-) 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 01daccfb..9364849d 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -144,5 +144,11 @@ "title.settings": "cat settings", "title.logout": "log out", "action.edit.profile": "edit cat profile", - "action.open.dm": "open direct meowchat" + "action.open.dm": "open direct meowchat", + "action.invite.accept": "accept meow invite", + "action.invite.cancel": "hiss away invite", + "action.invite.send": "invite to meowchat", + "action.profile.loading": "sniffing profile...", + "error.profile.load.failed": "failed to sniff profile.", + "unknown.author": "somecat" } \ 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 7b26bfb5..4fd12f81 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -144,5 +144,11 @@ "title.settings": "Settings", "title.logout": "Log out", "action.edit.profile": "Edit profile", - "action.open.dm": "Open DM" + "action.open.dm": "Open DM", + "action.invite.accept": "Accept invite", + "action.invite.cancel": "Cancel invite", + "action.invite.send": "Invite to DM", + "action.profile.loading": "Loading profile...", + "error.profile.load.failed": "Failed to load profile.", + "unknown.author": "Someone" } \ No newline at end of file diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 3c769c62..41253048 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -1467,7 +1467,7 @@ async function openProfile(accountId) { let popupInner = document.getElementById("profile-popup-inner"); //show loading - popupInner.innerHTML = '

Loading profile...

'; + popupInner.innerHTML = `

${processBlah('action.profile.loading')}

`; popupContainer.style.display = "flex"; //force reflow @@ -1496,9 +1496,77 @@ async function openProfile(accountId) { let bioHtml = bio && bio.trim() !== "" ? `
${bio.replace(//g, ">")}
` : ""; let isOwnProfile = accountId === id; - let actionBtnHtml = isOwnProfile ? - `` : - ``; + let actionBtnHtml = ""; + if (isOwnProfile) { + actionBtnHtml = ``; + } else { + let hasDm = false; + let dmRoomId = ""; + if (window.cachedDms) { + try { + let parsedDms = JSON.parse(window.cachedDms); + if (parsedDms.dms) { + for (let dmId in parsedDms.dms) { + let parts = dmId.split('_'); + if (parts.length >= 2) { + let accStandard = accountId.replace(':', ';'); + if (!accStandard.includes(';')) accStandard += ';' + host; + + let p0Standard = parts[0].replace(':', ';'); + if (!p0Standard.includes(';')) p0Standard += ';' + host; + + let p1Standard = parts[1].replace(':', ';'); + if (!p1Standard.includes(';')) p1Standard += ';' + host; + + if (p0Standard === accStandard || p1Standard === accStandard) { + hasDm = true; + dmRoomId = dmId; + break; + } + } + } + } + } catch(e){} + } + + if (hasDm) { + actionBtnHtml = ``; + } else { + let sentInvite = false; + let receivedInvite = false; + if (window.cachedInvites) { + let checkInvite = (type) => { + if (window.cachedInvites[type]) { + try { + let invites = JSON.parse(window.cachedInvites[type]); + if (invites.dms) { + for(let key in invites.dms) { + let accStandard = accountId.replace(':', ';'); + if (!accStandard.includes(';')) accStandard += ';' + host; + + let keyStandard = key.replace(':', ';'); + if (!keyStandard.includes(';')) keyStandard += ';' + host; + + if (keyStandard === accStandard) return true; + } + } + } catch(e){} + } + return false; + }; + sentInvite = checkInvite('sent'); + receivedInvite = checkInvite('received'); + } + + if (receivedInvite) { + actionBtnHtml = ``; + } else if (sentInvite) { + actionBtnHtml = ``; + } else { + actionBtnHtml = ``; + } + } + } popupInner.innerHTML = ` ${bannerHtml} @@ -1508,14 +1576,14 @@ async function openProfile(accountId) {
${profileName}
-
${profileName}:${profileHost} (${accountId})
+
${profileName}:${profileHost} (${accountId.split(':')[0]})
${bioHtml} ${actionBtnHtml}
`; } catch (e) { - popupInner.innerHTML = '

Failed to load profile.

'; + popupInner.innerHTML = `

${processBlah('error.profile.load.failed')}

`; console.error(e); } } @@ -1608,7 +1676,8 @@ async function renderInvites(res, type) { .replaceAll("{username}", username.split(':')[0]) .replaceAll("{pfp}", pfp) .replaceAll("{desc}", desc) - .replaceAll("{actions}", actions); + .replaceAll("{actions}", actions) + .replaceAll("{id}", id); html += entry; } @@ -1659,6 +1728,19 @@ async function switchInvitesTab(tab) { await loadInvites(tab); } +async function sendInviteTo(targetId) { + try { + showAction("action.invite.sending", "invite.action"); + let res = await fetchEncrypted("user/dm/invite", targetId); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.sent"); + if (typeof loadInvites === 'function') await loadInvites('sent'); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + async function acceptInvite(targetId) { //TODO: Implement key generation try { showAction("action.invite.accepting", "invite.action"); @@ -1769,10 +1851,12 @@ function gotoJoinSpace() { switchRoomContent("title.join.space", joinSpaceScreen, false); } -async function gotoHome() { +async function gotoHome(splash = true) { if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar setActiveSidebarIndicator(sidebarHomeIndicator); - switchRoomContent("title.splash", splashScreen, false); + if (splash) { + switchRoomContent("title.splash", splashScreen, false); + } switchRoomsBar("title.home", homeRoomBar); setActiveRoombarItem(null); setupWebSocket(); @@ -2010,6 +2094,11 @@ async function openDm(dmId, username, targetId) { let pfp = await getAvatarUrl(targetId, username); let iconHtml = ``; + + if (currentRoomsBarTitle !== "title.home") { + gotoHome(false); + } + await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml); let msgContainer = document.getElementById("chat-messages"); @@ -2155,7 +2244,7 @@ async function renderMessages(messages, isPrepend = false) { respondedAuthorId = parts[1]; } - let replyAuthor = "Someone"; + let replyAuthor = processBlah('unknown.author'); let needsAsyncFetch = false; if (loadedMessages[respondedId] && loadedMessages[respondedId].author !== "0") { @@ -2183,7 +2272,7 @@ async function renderMessages(messages, isPrepend = false) { window.replyIdCounter = (window.replyIdCounter || 0) + 1; if (window.replyIdCounter > 1000000) window.replyIdCounter = 0; randId = "reply-author-" + window.replyIdCounter; - replyAuthor = "Someone"; + replyAuthor = processBlah('unknown.author'); setTimeout(async () => { try { let aId = respondedAuthorId; @@ -2262,10 +2351,10 @@ async function renderMessages(messages, isPrepend = false) { html += `
- ${showAvatar ? `` : `
`} + ${showAvatar ? `` : `
`}
${showAvatar ? `
- ${authorName} + ${authorName} ${timeStr}
` : ""} ${repliedHtml} @@ -2307,8 +2396,16 @@ window.visualViewport?.addEventListener("resize", async () => { isAdjusting = true; while (isAdjusting) { + //chat scroll fix ignoreScroll = true; container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll; + + //context menu fix + let menuRect = fixedContextMenu.getBoundingClientRect(); + if (menuRect.bottom > window.innerHeight) { + fixedContextMenu.style.top = `${Math.max(10, window.innerHeight - menuRect.height - 10)}px`; + } + requestAnimationFrame(() => { ignoreScroll = false; }); @@ -2585,7 +2682,7 @@ async function replyMessage(msgId) { let bar = document.getElementById("replying-bar"); if (bar) { let msg = loadedMessages[msgId]; - let authorName = "Someone"; + let authorName = processBlah('unknown.author'); if (msg && msg.author !== "0" && dmUsernameCache[msg.author]) { authorName = dmUsernameCache[msg.author].split(':')[0]; } @@ -2670,16 +2767,7 @@ function openReactionPicker(e) { let rect = fixedContextMenu.getBoundingClientRect(); showFixedContextMenu({top: rect.top, right: rect.left, bottom: rect.top, left: rect.left}, reactionPickerMenuHtml); let searchInput = document.getElementById('reaction-picker-search'); - if (searchInput) { - searchInput.addEventListener('focus', () => { //TODO: use code from resize event - setTimeout(() => { - let menuRect = fixedContextMenu.getBoundingClientRect(); - if (menuRect.bottom > window.innerHeight) { - fixedContextMenu.style.top = `${Math.max(10, window.innerHeight - menuRect.height - 10)}px`; - } - }, 500); - }); - } + } async function reactMessagePrompt(msgId, quickReaction = null) { diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index d553490b..50575755 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -117,9 +117,9 @@ var notifisScreen = ` var invitesEntry = `
- +
- {username} + {username} {desc}
diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index a5b041df..e0a209d7 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -811,14 +811,14 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5); opacity: 0; transform: scale(0.6); - transition: transform 0.2s cubic-bezier(0.4, -0.3, 0.7, 1), opacity 0.2s ease; + transition: all 0.2s ease; display: flex; flex-direction: column; } .profile-popup-container.show .profile-popup-content { transform: scale(1); opacity: 1; - transition: transform 0.25s cubic-bezier(0.2, 1.15, 0.4, 1), opacity 0.25s ease; + transition: all 0.2s ease; } .profile-popup-close-btn { position: absolute; diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index 01daccfb..9364849d 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -144,5 +144,11 @@ "title.settings": "cat settings", "title.logout": "log out", "action.edit.profile": "edit cat profile", - "action.open.dm": "open direct meowchat" + "action.open.dm": "open direct meowchat", + "action.invite.accept": "accept meow invite", + "action.invite.cancel": "hiss away invite", + "action.invite.send": "invite to meowchat", + "action.profile.loading": "sniffing profile...", + "error.profile.load.failed": "failed to sniff profile.", + "unknown.author": "somecat" } \ No newline at end of file diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index 7b26bfb5..4fd12f81 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -144,5 +144,11 @@ "title.settings": "Settings", "title.logout": "Log out", "action.edit.profile": "Edit profile", - "action.open.dm": "Open DM" + "action.open.dm": "Open DM", + "action.invite.accept": "Accept invite", + "action.invite.cancel": "Cancel invite", + "action.invite.send": "Invite to DM", + "action.profile.loading": "Loading profile...", + "error.profile.load.failed": "Failed to load profile.", + "unknown.author": "Someone" } \ No newline at end of file diff --git a/webroot/main.js b/webroot/main.js index 3c769c62..41253048 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -1467,7 +1467,7 @@ async function openProfile(accountId) { let popupInner = document.getElementById("profile-popup-inner"); //show loading - popupInner.innerHTML = '

Loading profile...

'; + popupInner.innerHTML = `

${processBlah('action.profile.loading')}

`; popupContainer.style.display = "flex"; //force reflow @@ -1496,9 +1496,77 @@ async function openProfile(accountId) { let bioHtml = bio && bio.trim() !== "" ? `
${bio.replace(//g, ">")}
` : ""; let isOwnProfile = accountId === id; - let actionBtnHtml = isOwnProfile ? - `` : - ``; + let actionBtnHtml = ""; + if (isOwnProfile) { + actionBtnHtml = ``; + } else { + let hasDm = false; + let dmRoomId = ""; + if (window.cachedDms) { + try { + let parsedDms = JSON.parse(window.cachedDms); + if (parsedDms.dms) { + for (let dmId in parsedDms.dms) { + let parts = dmId.split('_'); + if (parts.length >= 2) { + let accStandard = accountId.replace(':', ';'); + if (!accStandard.includes(';')) accStandard += ';' + host; + + let p0Standard = parts[0].replace(':', ';'); + if (!p0Standard.includes(';')) p0Standard += ';' + host; + + let p1Standard = parts[1].replace(':', ';'); + if (!p1Standard.includes(';')) p1Standard += ';' + host; + + if (p0Standard === accStandard || p1Standard === accStandard) { + hasDm = true; + dmRoomId = dmId; + break; + } + } + } + } + } catch(e){} + } + + if (hasDm) { + actionBtnHtml = ``; + } else { + let sentInvite = false; + let receivedInvite = false; + if (window.cachedInvites) { + let checkInvite = (type) => { + if (window.cachedInvites[type]) { + try { + let invites = JSON.parse(window.cachedInvites[type]); + if (invites.dms) { + for(let key in invites.dms) { + let accStandard = accountId.replace(':', ';'); + if (!accStandard.includes(';')) accStandard += ';' + host; + + let keyStandard = key.replace(':', ';'); + if (!keyStandard.includes(';')) keyStandard += ';' + host; + + if (keyStandard === accStandard) return true; + } + } + } catch(e){} + } + return false; + }; + sentInvite = checkInvite('sent'); + receivedInvite = checkInvite('received'); + } + + if (receivedInvite) { + actionBtnHtml = ``; + } else if (sentInvite) { + actionBtnHtml = ``; + } else { + actionBtnHtml = ``; + } + } + } popupInner.innerHTML = ` ${bannerHtml} @@ -1508,14 +1576,14 @@ async function openProfile(accountId) {
${profileName}
-
${profileName}:${profileHost} (${accountId})
+
${profileName}:${profileHost} (${accountId.split(':')[0]})
${bioHtml} ${actionBtnHtml}
`; } catch (e) { - popupInner.innerHTML = '

Failed to load profile.

'; + popupInner.innerHTML = `

${processBlah('error.profile.load.failed')}

`; console.error(e); } } @@ -1608,7 +1676,8 @@ async function renderInvites(res, type) { .replaceAll("{username}", username.split(':')[0]) .replaceAll("{pfp}", pfp) .replaceAll("{desc}", desc) - .replaceAll("{actions}", actions); + .replaceAll("{actions}", actions) + .replaceAll("{id}", id); html += entry; } @@ -1659,6 +1728,19 @@ async function switchInvitesTab(tab) { await loadInvites(tab); } +async function sendInviteTo(targetId) { + try { + showAction("action.invite.sending", "invite.action"); + let res = await fetchEncrypted("user/dm/invite", targetId); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.sent"); + if (typeof loadInvites === 'function') await loadInvites('sent'); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + async function acceptInvite(targetId) { //TODO: Implement key generation try { showAction("action.invite.accepting", "invite.action"); @@ -1769,10 +1851,12 @@ function gotoJoinSpace() { switchRoomContent("title.join.space", joinSpaceScreen, false); } -async function gotoHome() { +async function gotoHome(splash = true) { if (roomsBarContainer) roomsBarContainer.style.display = ""; //show roombar setActiveSidebarIndicator(sidebarHomeIndicator); - switchRoomContent("title.splash", splashScreen, false); + if (splash) { + switchRoomContent("title.splash", splashScreen, false); + } switchRoomsBar("title.home", homeRoomBar); setActiveRoombarItem(null); setupWebSocket(); @@ -2010,6 +2094,11 @@ async function openDm(dmId, username, targetId) { let pfp = await getAvatarUrl(targetId, username); let iconHtml = ``; + + if (currentRoomsBarTitle !== "title.home") { + gotoHome(false); + } + await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml); let msgContainer = document.getElementById("chat-messages"); @@ -2155,7 +2244,7 @@ async function renderMessages(messages, isPrepend = false) { respondedAuthorId = parts[1]; } - let replyAuthor = "Someone"; + let replyAuthor = processBlah('unknown.author'); let needsAsyncFetch = false; if (loadedMessages[respondedId] && loadedMessages[respondedId].author !== "0") { @@ -2183,7 +2272,7 @@ async function renderMessages(messages, isPrepend = false) { window.replyIdCounter = (window.replyIdCounter || 0) + 1; if (window.replyIdCounter > 1000000) window.replyIdCounter = 0; randId = "reply-author-" + window.replyIdCounter; - replyAuthor = "Someone"; + replyAuthor = processBlah('unknown.author'); setTimeout(async () => { try { let aId = respondedAuthorId; @@ -2262,10 +2351,10 @@ async function renderMessages(messages, isPrepend = false) { html += `
- ${showAvatar ? `` : `
`} + ${showAvatar ? `` : `
`}
${showAvatar ? `
- ${authorName} + ${authorName} ${timeStr}
` : ""} ${repliedHtml} @@ -2307,8 +2396,16 @@ window.visualViewport?.addEventListener("resize", async () => { isAdjusting = true; while (isAdjusting) { + //chat scroll fix ignoreScroll = true; container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll; + + //context menu fix + let menuRect = fixedContextMenu.getBoundingClientRect(); + if (menuRect.bottom > window.innerHeight) { + fixedContextMenu.style.top = `${Math.max(10, window.innerHeight - menuRect.height - 10)}px`; + } + requestAnimationFrame(() => { ignoreScroll = false; }); @@ -2585,7 +2682,7 @@ async function replyMessage(msgId) { let bar = document.getElementById("replying-bar"); if (bar) { let msg = loadedMessages[msgId]; - let authorName = "Someone"; + let authorName = processBlah('unknown.author'); if (msg && msg.author !== "0" && dmUsernameCache[msg.author]) { authorName = dmUsernameCache[msg.author].split(':')[0]; } @@ -2670,16 +2767,7 @@ function openReactionPicker(e) { let rect = fixedContextMenu.getBoundingClientRect(); showFixedContextMenu({top: rect.top, right: rect.left, bottom: rect.top, left: rect.left}, reactionPickerMenuHtml); let searchInput = document.getElementById('reaction-picker-search'); - if (searchInput) { - searchInput.addEventListener('focus', () => { //TODO: use code from resize event - setTimeout(() => { - let menuRect = fixedContextMenu.getBoundingClientRect(); - if (menuRect.bottom > window.innerHeight) { - fixedContextMenu.style.top = `${Math.max(10, window.innerHeight - menuRect.height - 10)}px`; - } - }, 500); - }); - } + } async function reactMessagePrompt(msgId, quickReaction = null) { diff --git a/webroot/screens.js b/webroot/screens.js index d553490b..50575755 100644 --- a/webroot/screens.js +++ b/webroot/screens.js @@ -117,9 +117,9 @@ var notifisScreen = ` var invitesEntry = `
- +
- {username} + {username} {desc}
diff --git a/webroot/style.css b/webroot/style.css index a5b041df..e0a209d7 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -811,14 +811,14 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5); opacity: 0; transform: scale(0.6); - transition: transform 0.2s cubic-bezier(0.4, -0.3, 0.7, 1), opacity 0.2s ease; + transition: all 0.2s ease; display: flex; flex-direction: column; } .profile-popup-container.show .profile-popup-content { transform: scale(1); opacity: 1; - transition: transform 0.25s cubic-bezier(0.2, 1.15, 0.4, 1), opacity 0.25s ease; + transition: all 0.2s ease; } .profile-popup-close-btn { position: absolute;