add message pinning
This commit is contained in:
parent
91d293ee05
commit
340c16b080
10 changed files with 368 additions and 52 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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ść"
|
||||
}
|
||||
|
|
@ -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 = `<inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}`;
|
||||
let pinCallBtns = showBack ? `${callBtnHtml}` : `${pinBtnHtml}${callBtnHtml}`;
|
||||
let backBtn = showBack ? `<button class="topbar-action-btn" onclick="${backAction}" style="margin-right: 0.5rem">${ICONS.arrowBack({ width: "1.5rem", height: "1.5rem" })}</button>` : "";
|
||||
roomsTopBarTransition.innerHTML = `<inherit style="display: flex; align-items: center;">${backBtn}<inherit>${processBlah(title)}</inherit></inherit><div class="flex-spacer"></div>${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 += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="downloadAndRenderAttachment('${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'))">
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="event.stopPropagation(); downloadAndRenderAttachment(this, '${msgId}', '${att.id}', decodeURIComponent('${safeType}'), decodeURIComponent('${safeName}'))">
|
||||
<div style="padding: 1rem; text-align: center; color: var(--text-color);">
|
||||
${iconSvg}
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; word-break: break-all;">${escapeHtml(displayName)}</div>
|
||||
|
|
@ -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 = `<span onclick="event.stopPropagation(); scrollToPinnedMessage(event, '${msg.pervious}')" style="cursor: pointer; font-weight: bold; color: rgba(255, 255, 255, 0.9);">${processBlah('title.message.lowercase')}</span>`;
|
||||
let clickableAuthor = `<span onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer; font-weight: bold;">${authorName}</span>`;
|
||||
|
||||
let textStr = processBlah(actionStr);
|
||||
content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg);
|
||||
}
|
||||
html += `<div style="text-align: center; opacity: 0.5; padding: 1rem; font-size: 0.9rem;">${content}</div>`;
|
||||
lastAuthor = null;
|
||||
} else {
|
||||
|
|
@ -2560,7 +2589,7 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
}
|
||||
|
||||
let divIdStr = randId !== "" ? `id="${randId}" ` : "";
|
||||
repliedHtml = `<div ${divIdStr}class="chat-message-replied" style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.2rem; cursor: pointer;" onclick="scrollToMessage('${respondedId}')">${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}</div>`;
|
||||
repliedHtml = `<div ${divIdStr}class="chat-message-replied" style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.2rem; cursor: pointer;" onclick="event.stopPropagation(); scrollToMessage('${respondedId}')">${ICONS.cornerUpLeft({ width: "0.8rem", height: "0.8rem", stroke: "currentColor", style: "vertical-align: middle;" })} ${processBlah('title.replied.to')} ${replyAuthor}</div>`;
|
||||
}
|
||||
|
||||
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 += `<span class="reaction-pill" style="${pillStyle}" onclick="reactMessagePrompt('${msgId}', '${safeRx}')"><span>${processBlah(escapedRxVal)}</span>${rxCounts[rxVal].count > 1 ? `<span style="opacity: 0.5;">${rxCounts[rxVal].count}</span>` : ""}</span>`;
|
||||
reactionsHtml += `<span class="reaction-pill" style="${pillStyle}" onclick="event.stopPropagation(); reactMessagePrompt('${msgId}', '${safeRx}')"><span>${processBlah(escapedRxVal)}</span>${rxCounts[rxVal].count > 1 ? `<span style="opacity: 0.5;">${rxCounts[rxVal].count}</span>` : ""}</span>`;
|
||||
}
|
||||
reactionsHtml += `</div>`;
|
||||
}
|
||||
|
|
@ -2603,11 +2632,11 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
}
|
||||
|
||||
html += `
|
||||
<div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
|
||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp" onclick="openProfile('${msg.author}')" style="cursor: pointer;">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||
<div class="chat-message ${extraClass}${isPinnedContext ? ` chat-message-pinned-item` : ``}" data-msg-id="${msgId}" id="${isPinnedContext ? `pinned-` : ``}msg-${msgId}"${isPinnedContext ? ` onclick="scrollToPinnedMessage(event, '${msgId}')"` : ``} style="${isPinnedContext ? `cursor: pointer;` : ``}">
|
||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp" onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer;">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(arguments[0], '${msgId}')">
|
||||
${showAvatar ? `<div class="chat-message-header">
|
||||
<span class="chat-message-author" onclick="openProfile('${msg.author}')" style="cursor: pointer;">${authorName}</span>
|
||||
<span class="chat-message-author" onclick="event.stopPropagation(); openProfile('${msg.author}')" style="cursor: pointer;">${authorName}</span>
|
||||
<span class="chat-message-timestamp">${timeStr}</span>
|
||||
</div>` : ""}
|
||||
${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 = `
|
||||
<button onclick="togglePinMessage('${msgId}')">
|
||||
${ICONS.pin()}
|
||||
<span class="blah">${isPinned ? processBlah("title.unpin.message") : processBlah("title.pin.message")}</span>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
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 = `<div style="padding: 2rem; text-align: center; opacity: 0.5;"><blah>desc.messages.loading</blah></div>`;
|
||||
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", `<div style="padding: 2rem; text-align: center; opacity: 0.5;"><blah>error.pinned.messages.empty</blah></div>`, true, "restoreDmDetails()");
|
||||
return;
|
||||
}
|
||||
|
||||
let innerHtml = await generateMessagesHtml(entries, true);
|
||||
let html = `<div id="pinned-messages" style="display: flex; flex-direction: column; padding: 1rem; gap: 0.5rem; overflow-y: auto; height: 100%;">
|
||||
${innerHtml}
|
||||
</div>`;
|
||||
switchDetailsContent("title.pinned.messages", html, true, "restoreDmDetails()");
|
||||
} else {
|
||||
switchDetailsContent("title.pinned.messages", `<div style="padding: 2rem; text-align: center; opacity: 0.5;"><blah>error:message.not.found</blah></div>`, true, "restoreDmDetails()");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
switchDetailsContent("title.pinned.messages", `<div style="padding: 2rem; text-align: center; opacity: 0.5;"><blah>error.network.error</blah></div>`, 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,11 +263,12 @@ var messageContextMenu = `
|
|||
${ICONS.delete({ fill: "var(--big-red)" })}
|
||||
<span class="blah" style="color: var(--big-red)">title.delete.message</span>
|
||||
</button>
|
||||
<div id="context-pin-container"></div>
|
||||
`;
|
||||
|
||||
//elements
|
||||
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()">${ICONS.arrowBack({ width: "1.5rem", height: "1.5rem" })}</button>`;
|
||||
var pinBtnHtml = `<button class="topbar-action-btn room-action-pin right">${ICONS.pin({ width: "1.5rem", height: "1.5rem" })}</button>`;
|
||||
var pinBtnHtml = `<button class="topbar-action-btn room-action-pin right" onclick="showPinnedMessages()">${ICONS.pin({ width: "1.5rem", height: "1.5rem" })}</button>`;
|
||||
var callBtnHtml = `<button class="topbar-action-btn room-action-call right">${ICONS.call({ width: "1.5rem", height: "1.5rem" })}</button>`;
|
||||
|
||||
var settingsBar = `
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue