From 340c16b0803dd97698a93179b5cba5f82db6dd09 Mon Sep 17 00:00:00 2001 From: olcxja Date: Sat, 25 Jul 2026 01:06:45 +0200 Subject: [PATCH] add message pinning --- .../src/main/assets/public/blah/en-cat.json | 9 +- .../src/main/assets/public/blah/en-us.json | 9 +- .../src/main/assets/public/blah/pl-pl.json | 9 +- android/app/src/main/assets/public/main.js | 180 +++++++++++++++--- android/app/src/main/assets/public/screens.js | 3 +- webroot/blah/en-cat.json | 9 +- webroot/blah/en-us.json | 9 +- webroot/blah/pl-pl.json | 9 +- webroot/main.js | 180 +++++++++++++++--- webroot/screens.js | 3 +- 10 files changed, 368 insertions(+), 52 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 16927f31..e2d7812c 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -194,5 +194,12 @@ "placeholder.type.text.emoji": "Type purr / meow...", "action.cancel": "Cancel", "title.crop.image": "Crop Image", - "title.zoom": "Zoom" + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned meows", + "title.message.lowercase": "meowsage" } \ 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 dc3c41f2..21b5fcf2 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -194,5 +194,12 @@ "placeholder.type.text.emoji": "Type text / emoji...", "action.cancel": "Cancel", "title.crop.image": "Crop Image", - "title.zoom": "Zoom" + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned messages", + "title.message.lowercase": "message" } \ No newline at end of file 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 d59ae8b3..7ab7e4bf 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -194,5 +194,12 @@ "placeholder.type.text.emoji": "Wpisz tekst / emoji...", "action.cancel": "Anuluj", "title.crop.image": "Przytnij obraz", - "title.zoom": "Przybliżenie" + "title.zoom": "Przybliżenie", + "title.pin.message": "Przypnij", + "title.unpin.message": "Odepnij", + "message.pinned.by": "{0} przypiął {1}.", + "message.unpinned.by": "{0} odpiął {1}.", + "title.pinned.messages": "Przypięte", + "error.pinned.messages.empty": "Brak przypiętych wiadomości", + "title.message.lowercase": "wiadomość" } \ 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 6217d060..5334dbdd 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -1350,7 +1350,14 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob var currentDetailsContentTitle = null; var currentDetailsContentHtml = null; -async function switchDetailsContent(title, content) { +var savedDetailsHtml = null; +var savedDetailsTitle = null; + +async function switchDetailsContent(title, content, showBack = false, backAction = "", saveCurrent = false) { + if (saveCurrent && currentDetailsContentTitle !== title) { + savedDetailsHtml = currentDetailsContentHtml; + savedDetailsTitle = currentDetailsContentTitle; + } if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return; currentDetailsContentTitle = title; currentDetailsContentHtml = content; @@ -1364,13 +1371,14 @@ async function switchDetailsContent(title, content) { roomsTopBarTransition.style.opacity = "0"; const rem = parseFloat(getComputedStyle(document.documentElement).fontSize); - if (window.innerWidth > 55 * rem) { + if (window.innerWidth > 55 * rem || mainScreen.classList.contains('mobile-details')) { await delay(200); } - let pinCallBtns = `${pinBtnHtml}${callBtnHtml}`; - roomsTopBarTransition.innerHTML = `${processBlah(title)}
${pinCallBtns}`; + let pinCallBtns = showBack ? `${callBtnHtml}` : `${pinBtnHtml}${callBtnHtml}`; + let backBtn = showBack ? `` : ""; + roomsTopBarTransition.innerHTML = `${backBtn}${processBlah(title)}
${pinCallBtns}`; let parser = new DOMParser(); let doc = parser.parseFromString(content, "text/html"); @@ -1495,7 +1503,7 @@ document.addEventListener("click", (e) => { if (mainScreen && mainScreen.classList.contains('mobile-details')) { 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')) { + if (!roomDetailsBar.contains(e.target) && !e.target.closest('#room-topbar-clickable') && !e.target.closest('.profile-popup-container') && !e.target.closest('.room-action-pin')) { mobileNavBack(); } } @@ -2370,12 +2378,7 @@ function truncateFilename(name, maxLength = 20) { return baseName.substring(0, half) + "..." + baseName.substring(baseName.length - (allowedBaseLength - half)) + ext; } -async function renderMessages(messages, isPrepend = false) { - let container = document.getElementById("chat-messages"); - if (!container) return; - - let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); - +async function generateMessagesHtml(entries, isPinnedContext = false) { let html = ""; let lastAuthor = null; @@ -2419,7 +2422,7 @@ async function renderMessages(messages, isPrepend = false) { let safeType = encodeURIComponent(att.type).replace(/'/g, "%27"); let safeName = encodeURIComponent(att.name).replace(/'/g, "%27"); - attachmentsHtml += `
+ attachmentsHtml += `
${iconSvg}
${escapeHtml(displayName)}
@@ -2449,7 +2452,33 @@ async function renderMessages(messages, isPrepend = false) { // Also still process the whole thing in case it's just "dm.begin.notice" without {blah()} content = processBlah(content); - if (msg.type === "larp.info") { + if (msg.type && msg.type.startsWith("larp.info")) { + if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin") { + let authorName = "Unknown"; + if (msg.author && msg.author !== "0") { + if (!dmUsernameCache[msg.author]) { + let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`); + if (fullUsername && !fullUsername.startsWith("error:")) { + dmUsernameCache[msg.author] = fullUsername; + dmPfpCache[msg.author] = await getAvatarUrl(msg.author, fullUsername); + } + } + if (dmUsernameCache[msg.author]) { + authorName = escapeHtml(dmUsernameCache[msg.author].split(':')[0]); + } else { + authorName = escapeHtml(msg.author.split(';')[0]); + } + } + + let isPinned = msg.type === "larp.info.pin"; + let actionStr = isPinned ? "message.pinned.by" : "message.unpinned.by"; + + let clickableMsg = `${processBlah('title.message.lowercase')}`; + let clickableAuthor = `${authorName}`; + + let textStr = processBlah(actionStr); + content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + } html += `
${content}
`; lastAuthor = null; } else { @@ -2560,7 +2589,7 @@ async function renderMessages(messages, isPrepend = false) { } let divIdStr = randId !== "" ? `id="${randId}" ` : ""; - repliedHtml = `
${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}
`; + repliedHtml = `
${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}
`; } let reactionsHtml = ""; @@ -2593,7 +2622,7 @@ async function renderMessages(messages, isPrepend = false) { let safeRx = encodeURIComponent(rxVal).replace(/'/g, "%27"); let escapedRxVal = escapeHtml(rxVal); let pillStyle = `display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.1rem 0.45rem; border-radius: 1rem; font-size: 0.85rem; border: var(--border-width) solid var(--light-border-color); cursor: pointer; ${style}`; - reactionsHtml += `${processBlah(escapedRxVal)}${rxCounts[rxVal].count > 1 ? `${rxCounts[rxVal].count}` : ""}`; + reactionsHtml += `${processBlah(escapedRxVal)}${rxCounts[rxVal].count > 1 ? `${rxCounts[rxVal].count}` : ""}`; } reactionsHtml += `
`; } @@ -2603,11 +2632,11 @@ async function renderMessages(messages, isPrepend = false) { } html += ` -
- ${showAvatar ? `` : `
`} +
+ ${showAvatar ? `` : `
`}
${showAvatar ? `
- ${authorName} + ${authorName} ${timeStr}
` : ""} ${repliedHtml} @@ -2620,6 +2649,18 @@ async function renderMessages(messages, isPrepend = false) { } } + + return html; +} + + +async function renderMessages(messages, isPrepend = false) { + let container = document.getElementById("chat-messages"); + if (!container) return; + + let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); + let html = await generateMessagesHtml(entries, false); + const rem = parseFloat(getComputedStyle(document.documentElement).fontSize); let wasAtBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < (1.5 * rem); if (window.forceScrollToBottom) { @@ -2782,10 +2823,8 @@ function setupChatScrollListener() { -async function downloadAndRenderAttachment(msgId, attId, type, name) { +async function downloadAndRenderAttachment(container, msgId, attId, type, name) { if (!currentDmId || !currentDmKey) return; - - let container = document.getElementById(`att-container-${msgId}-${attId}`); if (!container) return; if (container.dataset.loaded === "true") { @@ -3110,6 +3149,9 @@ function setupWebSocket() { if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout); dmMessageLoadTimeout = setTimeout(() => { loadDmMessages(msgDmId); + if (currentDetailsContentTitle === "title.pinned.messages") { + showPinnedMessages(true, false); + } }, 300); } } @@ -3151,6 +3193,41 @@ function handleMessageContextMenu(e, msgId) { } } if (reactBtn) reactBtn.style.display = ""; + + let pinContainer = fixedContextMenu.querySelector("#context-pin-container"); + if (pinContainer) { + let isPinned = msg && msg.pinned === "true"; + pinContainer.innerHTML = ` + `; + } + } +} + + +async function togglePinMessage(msgId) { + if (fixedContextMenu) fixedContextMenu.classList.remove("show"); + if (typeof clearContextMenuStyles === "function") clearContextMenuStyles(); + + try { + let payload = JSON.stringify({ + string1: currentDmId, + string2: msgId + }); + await fetchEncrypted("dm/message/pin/toggle", payload); + } catch (e) { + console.error(e); + showBlahNotification("error.network.error"); + } +} + +async function restoreDmDetails() { + if (savedDetailsHtml && savedDetailsTitle) { + await switchDetailsContent(savedDetailsTitle, savedDetailsHtml); + savedDetailsHtml = null; + savedDetailsTitle = null; } } @@ -3934,4 +4011,63 @@ function handleDmInteraction() { document.addEventListener('click', handleDmInteraction); document.addEventListener('keydown', handleDmInteraction); -document.addEventListener('touchstart', handleDmInteraction); \ No newline at end of file +document.addEventListener('touchstart', handleDmInteraction); +async function showPinnedMessages(forceReload = false, showPanel = true) { + if (!forceReload && currentDetailsContentTitle === "title.pinned.messages" && savedDetailsHtml) { + if (showPanel) mobileNavDetails(); + return; + } + + if (showPanel) mobileNavDetails(); + // Do not show loading html on refresh if we are already in pinned messages, to avoid jarring flashing + if (!forceReload) { + let loadingHtml = `
desc.messages.loading
`; + switchDetailsContent("title.pinned.messages", loadingHtml, true, "restoreDmDetails()", true); + } + + try { + let res = await fetchEncrypted("dm/messages/pinned", currentDmId); + if (res && !res.startsWith("error:")) { + let msgs = JSON.parse(res); + let entries = Object.entries(msgs).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); + if (entries.length === 0) { + switchDetailsContent("title.pinned.messages", `
error.pinned.messages.empty
`, true, "restoreDmDetails()"); + return; + } + + let innerHtml = await generateMessagesHtml(entries, true); + let html = `
+ ${innerHtml} +
`; + switchDetailsContent("title.pinned.messages", html, true, "restoreDmDetails()"); + } else { + switchDetailsContent("title.pinned.messages", `
error:message.not.found
`, true, "restoreDmDetails()"); + } + } catch (e) { + console.error(e); + switchDetailsContent("title.pinned.messages", `
error.network.error
`, true, "restoreDmDetails()"); + } +} + +function scrollToPinnedMessage(e, msgId) { + if (e && e.target) { + if (e.target.closest('a') || + e.target.closest('button') || + e.target.closest('.attachment-box') || + e.target.closest('.chat-message-replied') || + e.target.closest('.reaction-pill') || + e.target.closest('.chat-message-author') || + e.target.closest('.chat-message-pfp')) { + return; + } + } + const rem = parseFloat(getComputedStyle(document.documentElement).fontSize); + if (window.innerWidth <= 55 * rem) { + mainScreen.classList.remove('mobile-details'); + mainScreen.classList.add('mobile-content'); + if (typeof setActiveSidebarIndicator === "function" && typeof currentMainIndicator !== "undefined") { + setActiveSidebarIndicator(currentMainIndicator); + } + } + scrollToMessage(msgId); +} diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index 7f08a3cf..84e3d765 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -263,11 +263,12 @@ var messageContextMenu = ` ${ICONS.delete({ fill: "var(--big-red)" })} title.delete.message +
`; //elements var backBtnHtml = ``; -var pinBtnHtml = ``; +var pinBtnHtml = ``; var callBtnHtml = ``; var settingsBar = ` diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index 16927f31..e2d7812c 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -194,5 +194,12 @@ "placeholder.type.text.emoji": "Type purr / meow...", "action.cancel": "Cancel", "title.crop.image": "Crop Image", - "title.zoom": "Zoom" + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned meows", + "title.message.lowercase": "meowsage" } \ No newline at end of file diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index dc3c41f2..21b5fcf2 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -194,5 +194,12 @@ "placeholder.type.text.emoji": "Type text / emoji...", "action.cancel": "Cancel", "title.crop.image": "Crop Image", - "title.zoom": "Zoom" + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned messages", + "title.message.lowercase": "message" } \ No newline at end of file diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json index d59ae8b3..7ab7e4bf 100644 --- a/webroot/blah/pl-pl.json +++ b/webroot/blah/pl-pl.json @@ -194,5 +194,12 @@ "placeholder.type.text.emoji": "Wpisz tekst / emoji...", "action.cancel": "Anuluj", "title.crop.image": "Przytnij obraz", - "title.zoom": "Przybliżenie" + "title.zoom": "Przybliżenie", + "title.pin.message": "Przypnij", + "title.unpin.message": "Odepnij", + "message.pinned.by": "{0} przypiął {1}.", + "message.unpinned.by": "{0} odpiął {1}.", + "title.pinned.messages": "Przypięte", + "error.pinned.messages.empty": "Brak przypiętych wiadomości", + "title.message.lowercase": "wiadomość" } \ No newline at end of file diff --git a/webroot/main.js b/webroot/main.js index 6217d060..5334dbdd 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -1350,7 +1350,14 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob var currentDetailsContentTitle = null; var currentDetailsContentHtml = null; -async function switchDetailsContent(title, content) { +var savedDetailsHtml = null; +var savedDetailsTitle = null; + +async function switchDetailsContent(title, content, showBack = false, backAction = "", saveCurrent = false) { + if (saveCurrent && currentDetailsContentTitle !== title) { + savedDetailsHtml = currentDetailsContentHtml; + savedDetailsTitle = currentDetailsContentTitle; + } if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return; currentDetailsContentTitle = title; currentDetailsContentHtml = content; @@ -1364,13 +1371,14 @@ async function switchDetailsContent(title, content) { roomsTopBarTransition.style.opacity = "0"; const rem = parseFloat(getComputedStyle(document.documentElement).fontSize); - if (window.innerWidth > 55 * rem) { + if (window.innerWidth > 55 * rem || mainScreen.classList.contains('mobile-details')) { await delay(200); } - let pinCallBtns = `${pinBtnHtml}${callBtnHtml}`; - roomsTopBarTransition.innerHTML = `${processBlah(title)}
${pinCallBtns}`; + let pinCallBtns = showBack ? `${callBtnHtml}` : `${pinBtnHtml}${callBtnHtml}`; + let backBtn = showBack ? `` : ""; + roomsTopBarTransition.innerHTML = `${backBtn}${processBlah(title)}
${pinCallBtns}`; let parser = new DOMParser(); let doc = parser.parseFromString(content, "text/html"); @@ -1495,7 +1503,7 @@ document.addEventListener("click", (e) => { if (mainScreen && mainScreen.classList.contains('mobile-details')) { 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')) { + if (!roomDetailsBar.contains(e.target) && !e.target.closest('#room-topbar-clickable') && !e.target.closest('.profile-popup-container') && !e.target.closest('.room-action-pin')) { mobileNavBack(); } } @@ -2370,12 +2378,7 @@ function truncateFilename(name, maxLength = 20) { return baseName.substring(0, half) + "..." + baseName.substring(baseName.length - (allowedBaseLength - half)) + ext; } -async function renderMessages(messages, isPrepend = false) { - let container = document.getElementById("chat-messages"); - if (!container) return; - - let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); - +async function generateMessagesHtml(entries, isPinnedContext = false) { let html = ""; let lastAuthor = null; @@ -2419,7 +2422,7 @@ async function renderMessages(messages, isPrepend = false) { let safeType = encodeURIComponent(att.type).replace(/'/g, "%27"); let safeName = encodeURIComponent(att.name).replace(/'/g, "%27"); - attachmentsHtml += `
+ attachmentsHtml += `
${iconSvg}
${escapeHtml(displayName)}
@@ -2449,7 +2452,33 @@ async function renderMessages(messages, isPrepend = false) { // Also still process the whole thing in case it's just "dm.begin.notice" without {blah()} content = processBlah(content); - if (msg.type === "larp.info") { + if (msg.type && msg.type.startsWith("larp.info")) { + if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin") { + let authorName = "Unknown"; + if (msg.author && msg.author !== "0") { + if (!dmUsernameCache[msg.author]) { + let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`); + if (fullUsername && !fullUsername.startsWith("error:")) { + dmUsernameCache[msg.author] = fullUsername; + dmPfpCache[msg.author] = await getAvatarUrl(msg.author, fullUsername); + } + } + if (dmUsernameCache[msg.author]) { + authorName = escapeHtml(dmUsernameCache[msg.author].split(':')[0]); + } else { + authorName = escapeHtml(msg.author.split(';')[0]); + } + } + + let isPinned = msg.type === "larp.info.pin"; + let actionStr = isPinned ? "message.pinned.by" : "message.unpinned.by"; + + let clickableMsg = `${processBlah('title.message.lowercase')}`; + let clickableAuthor = `${authorName}`; + + let textStr = processBlah(actionStr); + content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + } html += `
${content}
`; lastAuthor = null; } else { @@ -2560,7 +2589,7 @@ async function renderMessages(messages, isPrepend = false) { } let divIdStr = randId !== "" ? `id="${randId}" ` : ""; - repliedHtml = `
${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}
`; + repliedHtml = `
${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}
`; } let reactionsHtml = ""; @@ -2593,7 +2622,7 @@ async function renderMessages(messages, isPrepend = false) { let safeRx = encodeURIComponent(rxVal).replace(/'/g, "%27"); let escapedRxVal = escapeHtml(rxVal); let pillStyle = `display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.1rem 0.45rem; border-radius: 1rem; font-size: 0.85rem; border: var(--border-width) solid var(--light-border-color); cursor: pointer; ${style}`; - reactionsHtml += `${processBlah(escapedRxVal)}${rxCounts[rxVal].count > 1 ? `${rxCounts[rxVal].count}` : ""}`; + reactionsHtml += `${processBlah(escapedRxVal)}${rxCounts[rxVal].count > 1 ? `${rxCounts[rxVal].count}` : ""}`; } reactionsHtml += `
`; } @@ -2603,11 +2632,11 @@ async function renderMessages(messages, isPrepend = false) { } html += ` -
- ${showAvatar ? `` : `
`} +
+ ${showAvatar ? `` : `
`}
${showAvatar ? `
- ${authorName} + ${authorName} ${timeStr}
` : ""} ${repliedHtml} @@ -2620,6 +2649,18 @@ async function renderMessages(messages, isPrepend = false) { } } + + return html; +} + + +async function renderMessages(messages, isPrepend = false) { + let container = document.getElementById("chat-messages"); + if (!container) return; + + let entries = Object.entries(messages).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); + let html = await generateMessagesHtml(entries, false); + const rem = parseFloat(getComputedStyle(document.documentElement).fontSize); let wasAtBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < (1.5 * rem); if (window.forceScrollToBottom) { @@ -2782,10 +2823,8 @@ function setupChatScrollListener() { -async function downloadAndRenderAttachment(msgId, attId, type, name) { +async function downloadAndRenderAttachment(container, msgId, attId, type, name) { if (!currentDmId || !currentDmKey) return; - - let container = document.getElementById(`att-container-${msgId}-${attId}`); if (!container) return; if (container.dataset.loaded === "true") { @@ -3110,6 +3149,9 @@ function setupWebSocket() { if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout); dmMessageLoadTimeout = setTimeout(() => { loadDmMessages(msgDmId); + if (currentDetailsContentTitle === "title.pinned.messages") { + showPinnedMessages(true, false); + } }, 300); } } @@ -3151,6 +3193,41 @@ function handleMessageContextMenu(e, msgId) { } } if (reactBtn) reactBtn.style.display = ""; + + let pinContainer = fixedContextMenu.querySelector("#context-pin-container"); + if (pinContainer) { + let isPinned = msg && msg.pinned === "true"; + pinContainer.innerHTML = ` + `; + } + } +} + + +async function togglePinMessage(msgId) { + if (fixedContextMenu) fixedContextMenu.classList.remove("show"); + if (typeof clearContextMenuStyles === "function") clearContextMenuStyles(); + + try { + let payload = JSON.stringify({ + string1: currentDmId, + string2: msgId + }); + await fetchEncrypted("dm/message/pin/toggle", payload); + } catch (e) { + console.error(e); + showBlahNotification("error.network.error"); + } +} + +async function restoreDmDetails() { + if (savedDetailsHtml && savedDetailsTitle) { + await switchDetailsContent(savedDetailsTitle, savedDetailsHtml); + savedDetailsHtml = null; + savedDetailsTitle = null; } } @@ -3934,4 +4011,63 @@ function handleDmInteraction() { document.addEventListener('click', handleDmInteraction); document.addEventListener('keydown', handleDmInteraction); -document.addEventListener('touchstart', handleDmInteraction); \ No newline at end of file +document.addEventListener('touchstart', handleDmInteraction); +async function showPinnedMessages(forceReload = false, showPanel = true) { + if (!forceReload && currentDetailsContentTitle === "title.pinned.messages" && savedDetailsHtml) { + if (showPanel) mobileNavDetails(); + return; + } + + if (showPanel) mobileNavDetails(); + // Do not show loading html on refresh if we are already in pinned messages, to avoid jarring flashing + if (!forceReload) { + let loadingHtml = `
desc.messages.loading
`; + switchDetailsContent("title.pinned.messages", loadingHtml, true, "restoreDmDetails()", true); + } + + try { + let res = await fetchEncrypted("dm/messages/pinned", currentDmId); + if (res && !res.startsWith("error:")) { + let msgs = JSON.parse(res); + let entries = Object.entries(msgs).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); + if (entries.length === 0) { + switchDetailsContent("title.pinned.messages", `
error.pinned.messages.empty
`, true, "restoreDmDetails()"); + return; + } + + let innerHtml = await generateMessagesHtml(entries, true); + let html = `
+ ${innerHtml} +
`; + switchDetailsContent("title.pinned.messages", html, true, "restoreDmDetails()"); + } else { + switchDetailsContent("title.pinned.messages", `
error:message.not.found
`, true, "restoreDmDetails()"); + } + } catch (e) { + console.error(e); + switchDetailsContent("title.pinned.messages", `
error.network.error
`, true, "restoreDmDetails()"); + } +} + +function scrollToPinnedMessage(e, msgId) { + if (e && e.target) { + if (e.target.closest('a') || + e.target.closest('button') || + e.target.closest('.attachment-box') || + e.target.closest('.chat-message-replied') || + e.target.closest('.reaction-pill') || + e.target.closest('.chat-message-author') || + e.target.closest('.chat-message-pfp')) { + return; + } + } + const rem = parseFloat(getComputedStyle(document.documentElement).fontSize); + if (window.innerWidth <= 55 * rem) { + mainScreen.classList.remove('mobile-details'); + mainScreen.classList.add('mobile-content'); + if (typeof setActiveSidebarIndicator === "function" && typeof currentMainIndicator !== "undefined") { + setActiveSidebarIndicator(currentMainIndicator); + } + } + scrollToMessage(msgId); +} diff --git a/webroot/screens.js b/webroot/screens.js index 7f08a3cf..84e3d765 100644 --- a/webroot/screens.js +++ b/webroot/screens.js @@ -263,11 +263,12 @@ var messageContextMenu = ` ${ICONS.delete({ fill: "var(--big-red)" })} title.delete.message +
`; //elements var backBtnHtml = ``; -var pinBtnHtml = ``; +var pinBtnHtml = ``; var callBtnHtml = ``; var settingsBar = `