Fixed context menu flicker, resize scrolling and race conditions
All checks were successful
Android Build / publish (push) Successful in 50s
Linux Build / publish (push) Successful in 51s

This commit is contained in:
olcxja 2026-06-13 07:19:12 +02:00
commit 3f0f628569
10 changed files with 140 additions and 118 deletions

View file

@ -45,15 +45,12 @@
"password.cant.empty": "meow word cannot be empty", "password.cant.empty": "meow word cannot be empty",
"username.cant.empty": "cat name cannot be empty", "username.cant.empty": "cat name cannot be empty",
"bad.request": "bad request!!!", "bad.request": "bad request!!!",
"letters": "letters", "letters": "letters",
"numbers": "numbers", "numbers": "numbers",
"underscores": "underscores", "underscores": "underscores",
"loading.connecting": "connecting...", "loading.connecting": "connecting...",
"loading.loading": "loading...", "loading.loading": "loading...",
"loading.done": "ready! :3", "loading.done": "ready! :3",
"title.home": "bed", "title.home": "bed",
"title.dms": "direct meowchats", "title.dms": "direct meowchats",
"title.groups": "clowder", "title.groups": "clowder",
@ -80,7 +77,6 @@
"title.sent": "sent", "title.sent": "sent",
"title.all": "all", "title.all": "all",
"title.unread": "unread", "title.unread": "unread",
"desc.no.invites": "no invites found :c", "desc.no.invites": "no invites found :c",
"desc.no.notifications": "no notifications found :c", "desc.no.notifications": "no notifications found :c",
"desc.fetching.invites": "fetching invites...", "desc.fetching.invites": "fetching invites...",
@ -89,7 +85,6 @@
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat", "desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
"desc.invite.group.received": "{0} ({1}) invited you to a clowder", "desc.invite.group.received": "{0} ({1}) invited you to a clowder",
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder", "desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
"action.fetching.invites.sent": "fetching sent invites...", "action.fetching.invites.sent": "fetching sent invites...",
"action.fetching.invites.recv": "fetching received invites...", "action.fetching.invites.recv": "fetching received invites...",
"action.dm.fetch": "fetching direct meowchats...", "action.dm.fetch": "fetching direct meowchats...",
@ -98,7 +93,6 @@
"action.invite.revoking": "revoking invite...", "action.invite.revoking": "revoking invite...",
"action.invite.accepting": "accepting invite...", "action.invite.accepting": "accepting invite...",
"action.invite.declining": "declining invite...", "action.invite.declining": "declining invite...",
"title.sign.up": "sign up", "title.sign.up": "sign up",
"title.sign.in": "sign in", "title.sign.in": "sign in",
"title.sign.in.to": "sign in to your larpix instance", "title.sign.in.to": "sign in to your larpix instance",
@ -132,6 +126,7 @@
"title.react.message": "purr", "title.react.message": "purr",
"title.delete.message": "hiss away", "title.delete.message": "hiss away",
"title.replied.to": "meowed back to", "title.replied.to": "meowed back to",
"title.replying.to": "meowing back to",
"action.message.deleting": "hissing message away...", "action.message.deleting": "hissing message away...",
"action.message.sending": "sending meow...", "action.message.sending": "sending meow...",
"action.message.reacting": "adding purr...", "action.message.reacting": "adding purr...",
@ -140,5 +135,8 @@
"error:message.react.failed": "failed to add purr", "error:message.react.failed": "failed to add purr",
"info.sending.message": "sending...", "info.sending.message": "sending...",
"placeholder.message.input": "meow...", "placeholder.message.input": "meow...",
"larp.redacted": "this meow was hissed away." "larp.redacted": "this meow was hissed away.",
"placeholder.invitation.code": "invitation code meow",
"info.messages.loading.older": "Loading older meowsages...",
"error:message.not.found": "Meowsage not found"
} }

View file

@ -45,15 +45,12 @@
"password.cant.empty": "Password cannot be empty", "password.cant.empty": "Password cannot be empty",
"username.cant.empty": "Username cannot be empty", "username.cant.empty": "Username cannot be empty",
"bad.request": "Bad request", "bad.request": "Bad request",
"letters": "letters", "letters": "letters",
"numbers": "numbers", "numbers": "numbers",
"underscores": "underscores", "underscores": "underscores",
"loading.connecting": "Connecting...", "loading.connecting": "Connecting...",
"loading.loading": "Loading...", "loading.loading": "Loading...",
"loading.done": "Ready!", "loading.done": "Ready!",
"title.home": "Home", "title.home": "Home",
"title.dms": "Direct messages", "title.dms": "Direct messages",
"title.groups": "Groups", "title.groups": "Groups",
@ -88,7 +85,6 @@
"desc.invite.dm.sent": "You invited {0} ({1}) to chat", "desc.invite.dm.sent": "You invited {0} ({1}) to chat",
"desc.invite.group.received": "{0} ({1}) invited you to a group", "desc.invite.group.received": "{0} ({1}) invited you to a group",
"desc.invite.group.sent": "You invited {0} ({1}) to a group", "desc.invite.group.sent": "You invited {0} ({1}) to a group",
"action.fetching.invites.sent": "Fetching sent invites...", "action.fetching.invites.sent": "Fetching sent invites...",
"action.fetching.invites.recv": "Fetching received invites...", "action.fetching.invites.recv": "Fetching received invites...",
"action.dm.fetch": "Fetching dms...", "action.dm.fetch": "Fetching dms...",
@ -97,7 +93,6 @@
"action.invite.revoking": "Revoking invite...", "action.invite.revoking": "Revoking invite...",
"action.invite.accepting": "Accepting invite...", "action.invite.accepting": "Accepting invite...",
"action.invite.declining": "Declining invite...", "action.invite.declining": "Declining invite...",
"title.sign.up": "Sign Up", "title.sign.up": "Sign Up",
"title.sign.in": "Sign In", "title.sign.in": "Sign In",
"title.sign.in.to": "Sign in to your larpix instance", "title.sign.in.to": "Sign in to your larpix instance",
@ -132,6 +127,7 @@
"title.react.message": "React", "title.react.message": "React",
"title.delete.message": "Delete", "title.delete.message": "Delete",
"title.replied.to": "Replied to", "title.replied.to": "Replied to",
"title.replying.to": "Replying to",
"action.message.deleting": "Deleting message...", "action.message.deleting": "Deleting message...",
"action.message.sending": "Sending message...", "action.message.sending": "Sending message...",
"action.message.reacting": "Adding reaction...", "action.message.reacting": "Adding reaction...",
@ -139,5 +135,8 @@
"error:message.send.failed": "Failed to send message", "error:message.send.failed": "Failed to send message",
"error:message.react.failed": "Failed to add reaction", "error:message.react.failed": "Failed to add reaction",
"info.sending.message": "Sending...", "info.sending.message": "Sending...",
"larp.redacted": "This message was deleted." "larp.redacted": "This message was deleted.",
"placeholder.invitation.code": "invitation code",
"info.messages.loading.older": "Loading older messages...",
"error:message.not.found": "Message not found"
} }

View file

@ -1750,6 +1750,9 @@ document.addEventListener('touchmove', e => {
document.addEventListener('touchend', e => { document.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].screenX; touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY; touchEndY = e.changedTouches[0].screenY;
isAdjusting = false; //resize scroll fix
handleMobileSwipe(); handleMobileSwipe();
if (activeTouchButton) { if (activeTouchButton) {
@ -1985,7 +1988,7 @@ async function renderMessages(messages, isPrepend = false) {
if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes); if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
decrypted = true; decrypted = true;
} catch (e) { } catch (e) {
content = `<span style="color:var(--error-color)"><blah>error:messages.decrypt.failed</blah></span>`; content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
decrypted = false; decrypted = false;
} }
} else { } else {
@ -2145,16 +2148,20 @@ async function renderMessages(messages, isPrepend = false) {
} }
} }
let isRedacted = msg.type === "larp.redacted";
let redactedStyle = isRedacted ? ` opacity: 0.5; font-style: italic;` : ``;
let redactedIcon = isRedacted ? `<svg xmlns="http://www.w3.org/2000/svg" height="1rem" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: -0.165em; margin-right: 0.2rem;"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>` : ``;
html += ` html += `
<div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}"> <div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`} ${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')" ontouchstart="handleMessageTouchStart(event, '${msgId}')" ontouchend="handleMessageTouchEnd()" ontouchmove="handleMessageTouchMove()"> <div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')">
${showAvatar ? `<div class="chat-message-header"> ${showAvatar ? `<div class="chat-message-header">
<span class="chat-message-author">${authorName}</span> <span class="chat-message-author">${authorName}</span>
<span class="chat-message-timestamp">${timeStr}</span> <span class="chat-message-timestamp">${timeStr}</span>
</div>` : ""} </div>` : ""}
${repliedHtml} ${repliedHtml}
<div class="chat-message-text">${content}</div> <div class="chat-message-text" style="${redactedStyle}">${redactedIcon}${content}</div>
${reactionsHtml} ${reactionsHtml}
</div> </div>
</div> </div>
@ -2197,7 +2204,7 @@ window.visualViewport?.addEventListener("resize", async () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
ignoreScroll = false; ignoreScroll = false;
}); });
await delay(10); await delay(1);
} }
}); });
function setupChatScrollListener() { function setupChatScrollListener() {
@ -2277,6 +2284,23 @@ async function sendMessage() {
showBlahNotification(res); showBlahNotification(res);
let pElem = document.getElementById(pendingMsgId); let pElem = document.getElementById(pendingMsgId);
if (pElem) pElem.remove(); if (pElem) pElem.remove();
} else if (res && res.startsWith("success:")) {
let parts = res.split(":");
if (parts.length > 1 && parts[1] !== "message.sent") {
let newMsgId = parts[1];
loadedMessages[newMsgId] = {
author: id,
timestamp: Date.now().toString(),
type: "larp.text",
content: encryptedContent,
attachment: "",
key: "0",
pervious: "",
responded: replyingToMsgId || "",
reactions: ""
};
renderMessages(loadedMessages);
}
} }
} catch (e) { } catch (e) {
clearAction("msgsending"); clearAction("msgsending");
@ -2309,20 +2333,28 @@ function handleChatInputKey(event) {
let dmMessagePollInterval = null; let dmMessagePollInterval = null;
let appWebSocket = null; let appWebSocket = null;
let dmMessageLoadTimeout = null;
function setupWebSocket() { function setupWebSocket() {
if (appWebSocket && appWebSocket.readyState === WebSocket.OPEN) return; if (appWebSocket && (appWebSocket.readyState === WebSocket.OPEN || appWebSocket.readyState === WebSocket.CONNECTING)) return;
let wsUrl = url.replace(/^http/, "ws") + "/ws"; let wsUrl = url.replace(/^http/, "ws") + "/ws";
appWebSocket = new WebSocket(wsUrl); appWebSocket = new WebSocket(wsUrl);
appWebSocket.onopen = async () => { appWebSocket.onopen = async () => {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try { try {
let nonce = await getNonce(id, passwordHash); let nonce = await getNonce(id, passwordHash);
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce); let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc})); appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
} catch (e) { } catch (e) {
console.error("WS auth failed", e); console.error("WS auth failed", e);
} finally {
release();
} }
}; };
@ -2331,7 +2363,10 @@ function setupWebSocket() {
if (data.startsWith("dm_message:")) { if (data.startsWith("dm_message:")) {
let msgDmId = data.substring("dm_message:".length); let msgDmId = data.substring("dm_message:".length);
if (currentDmId === msgDmId) { if (currentDmId === msgDmId) {
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
dmMessageLoadTimeout = setTimeout(() => {
loadDmMessages(msgDmId); loadDmMessages(msgDmId);
}, 300);
} }
} }
}; };
@ -2342,11 +2377,14 @@ function setupWebSocket() {
} }
let currentContextMenuMsgId = null; let currentContextMenuMsgId = null;
let touchTimeout = null; let touchContextMenuFired = false;
let touchTarget = null;
function handleMessageContextMenu(e, msgId) { function handleMessageContextMenu(e, msgId) {
e.preventDefault(); e.preventDefault();
if (touchContextMenuFired) {
touchContextMenuFired = false;
return;
}
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles(); if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
currentContextMenuMsgId = msgId; currentContextMenuMsgId = msgId;
let elem = document.getElementById(`msg-${msgId}`); let elem = document.getElementById(`msg-${msgId}`);
@ -2363,40 +2401,6 @@ function handleMessageContextMenu(e, msgId) {
} }
} }
function handleMessageTouchStart(e, msgId) {
touchTarget = e.target;
touchTimeout = setTimeout(() => {
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
currentContextMenuMsgId = msgId;
let elem = document.getElementById(`msg-${msgId}`);
if (elem) elem.classList.add("context-menu-open");
let touch = e.touches[0];
showFixedContextMenu({
top: touch.clientY,
right: touch.clientX,
bottom: touch.clientY,
left: touch.clientX
}, messageContextMenu);
let delBtn = fixedContextMenu.querySelector("#context-delete-btn");
if (delBtn) {
if (loadedMessages[msgId] && loadedMessages[msgId].author !== id) {
delBtn.style.display = "none";
} else {
delBtn.style.display = "";
}
}
}, 500);
}
function handleMessageTouchEnd() {
if (touchTimeout) clearTimeout(touchTimeout);
}
function handleMessageTouchMove() {
if (touchTimeout) clearTimeout(touchTimeout);
}
async function deleteMessage(msgId) { async function deleteMessage(msgId) {
if (fixedContextMenu) fixedContextMenu.classList.remove("show"); if (fixedContextMenu) fixedContextMenu.classList.remove("show");
showAction("action.message.deleting", "msgdel"); showAction("action.message.deleting", "msgdel");
@ -2535,6 +2539,11 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes); let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
if (currentMsg) {
currentMsg.reactions = encryptedReactions;
renderMessages(loadedMessages);
}
let msgPayload = { let msgPayload = {
string1: currentDmId, string1: currentDmId,
string2: msgId, string2: msgId,

View file

@ -133,9 +133,9 @@ var chatScreen = `
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;"> <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;"> <div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
</div> </div>
<div id="replying-bar" style="display: none; padding: 0.5rem 1rem; border-top: var(--border-width) solid var(--light-border-color); font-size: 0.85rem; justify-content: space-between; align-items: center; background: var(--main-bg-color);"> <div id="replying-bar" style="display: none; padding: 0 1rem; border-top: var(--border-width) solid var(--light-border-color); font-size: 0.85rem; justify-content: space-between; align-items: center; background: var(--main-bg-color);">
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;"> <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;">
<span style="opacity: 0.7;"><blah>title.replied.to</blah> </span><strong id="replying-to-name"></strong>: <span id="replying-to-text" style="opacity: 0.7;"></span> <span style="opacity: 0.7;"><blah>title.replying.to</blah> </span><strong id="replying-to-name"></strong>: <span id="replying-to-text" style="opacity: 0.7;"></span>
</div> </div>
<button onclick="cancelReply()" style="background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0.2rem; margin-left: 0.5rem;"> <button onclick="cancelReply()" style="background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0.2rem; margin-left: 0.5rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" viewBox="0 -960 960 960" fill="currentColor"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" viewBox="0 -960 960 960" fill="currentColor"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>

View file

@ -159,6 +159,11 @@ indicator.active {
border-radius: 0.65rem; border-radius: 0.65rem;
} }
.reaction-pill {
backface-visibility: hidden;
transform: translateZ(0);
}
roomcontent { roomcontent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -45,15 +45,12 @@
"password.cant.empty": "meow word cannot be empty", "password.cant.empty": "meow word cannot be empty",
"username.cant.empty": "cat name cannot be empty", "username.cant.empty": "cat name cannot be empty",
"bad.request": "bad request!!!", "bad.request": "bad request!!!",
"letters": "letters", "letters": "letters",
"numbers": "numbers", "numbers": "numbers",
"underscores": "underscores", "underscores": "underscores",
"loading.connecting": "connecting...", "loading.connecting": "connecting...",
"loading.loading": "loading...", "loading.loading": "loading...",
"loading.done": "ready! :3", "loading.done": "ready! :3",
"title.home": "bed", "title.home": "bed",
"title.dms": "direct meowchats", "title.dms": "direct meowchats",
"title.groups": "clowder", "title.groups": "clowder",
@ -80,7 +77,6 @@
"title.sent": "sent", "title.sent": "sent",
"title.all": "all", "title.all": "all",
"title.unread": "unread", "title.unread": "unread",
"desc.no.invites": "no invites found :c", "desc.no.invites": "no invites found :c",
"desc.no.notifications": "no notifications found :c", "desc.no.notifications": "no notifications found :c",
"desc.fetching.invites": "fetching invites...", "desc.fetching.invites": "fetching invites...",
@ -89,7 +85,6 @@
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat", "desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
"desc.invite.group.received": "{0} ({1}) invited you to a clowder", "desc.invite.group.received": "{0} ({1}) invited you to a clowder",
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder", "desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
"action.fetching.invites.sent": "fetching sent invites...", "action.fetching.invites.sent": "fetching sent invites...",
"action.fetching.invites.recv": "fetching received invites...", "action.fetching.invites.recv": "fetching received invites...",
"action.dm.fetch": "fetching direct meowchats...", "action.dm.fetch": "fetching direct meowchats...",
@ -98,7 +93,6 @@
"action.invite.revoking": "revoking invite...", "action.invite.revoking": "revoking invite...",
"action.invite.accepting": "accepting invite...", "action.invite.accepting": "accepting invite...",
"action.invite.declining": "declining invite...", "action.invite.declining": "declining invite...",
"title.sign.up": "sign up", "title.sign.up": "sign up",
"title.sign.in": "sign in", "title.sign.in": "sign in",
"title.sign.in.to": "sign in to your larpix instance", "title.sign.in.to": "sign in to your larpix instance",
@ -132,6 +126,7 @@
"title.react.message": "purr", "title.react.message": "purr",
"title.delete.message": "hiss away", "title.delete.message": "hiss away",
"title.replied.to": "meowed back to", "title.replied.to": "meowed back to",
"title.replying.to": "meowing back to",
"action.message.deleting": "hissing message away...", "action.message.deleting": "hissing message away...",
"action.message.sending": "sending meow...", "action.message.sending": "sending meow...",
"action.message.reacting": "adding purr...", "action.message.reacting": "adding purr...",
@ -140,5 +135,8 @@
"error:message.react.failed": "failed to add purr", "error:message.react.failed": "failed to add purr",
"info.sending.message": "sending...", "info.sending.message": "sending...",
"placeholder.message.input": "meow...", "placeholder.message.input": "meow...",
"larp.redacted": "this meow was hissed away." "larp.redacted": "this meow was hissed away.",
"placeholder.invitation.code": "invitation code meow",
"info.messages.loading.older": "Loading older meowsages...",
"error:message.not.found": "Meowsage not found"
} }

View file

@ -45,15 +45,12 @@
"password.cant.empty": "Password cannot be empty", "password.cant.empty": "Password cannot be empty",
"username.cant.empty": "Username cannot be empty", "username.cant.empty": "Username cannot be empty",
"bad.request": "Bad request", "bad.request": "Bad request",
"letters": "letters", "letters": "letters",
"numbers": "numbers", "numbers": "numbers",
"underscores": "underscores", "underscores": "underscores",
"loading.connecting": "Connecting...", "loading.connecting": "Connecting...",
"loading.loading": "Loading...", "loading.loading": "Loading...",
"loading.done": "Ready!", "loading.done": "Ready!",
"title.home": "Home", "title.home": "Home",
"title.dms": "Direct messages", "title.dms": "Direct messages",
"title.groups": "Groups", "title.groups": "Groups",
@ -88,7 +85,6 @@
"desc.invite.dm.sent": "You invited {0} ({1}) to chat", "desc.invite.dm.sent": "You invited {0} ({1}) to chat",
"desc.invite.group.received": "{0} ({1}) invited you to a group", "desc.invite.group.received": "{0} ({1}) invited you to a group",
"desc.invite.group.sent": "You invited {0} ({1}) to a group", "desc.invite.group.sent": "You invited {0} ({1}) to a group",
"action.fetching.invites.sent": "Fetching sent invites...", "action.fetching.invites.sent": "Fetching sent invites...",
"action.fetching.invites.recv": "Fetching received invites...", "action.fetching.invites.recv": "Fetching received invites...",
"action.dm.fetch": "Fetching dms...", "action.dm.fetch": "Fetching dms...",
@ -97,7 +93,6 @@
"action.invite.revoking": "Revoking invite...", "action.invite.revoking": "Revoking invite...",
"action.invite.accepting": "Accepting invite...", "action.invite.accepting": "Accepting invite...",
"action.invite.declining": "Declining invite...", "action.invite.declining": "Declining invite...",
"title.sign.up": "Sign Up", "title.sign.up": "Sign Up",
"title.sign.in": "Sign In", "title.sign.in": "Sign In",
"title.sign.in.to": "Sign in to your larpix instance", "title.sign.in.to": "Sign in to your larpix instance",
@ -132,6 +127,7 @@
"title.react.message": "React", "title.react.message": "React",
"title.delete.message": "Delete", "title.delete.message": "Delete",
"title.replied.to": "Replied to", "title.replied.to": "Replied to",
"title.replying.to": "Replying to",
"action.message.deleting": "Deleting message...", "action.message.deleting": "Deleting message...",
"action.message.sending": "Sending message...", "action.message.sending": "Sending message...",
"action.message.reacting": "Adding reaction...", "action.message.reacting": "Adding reaction...",
@ -139,5 +135,8 @@
"error:message.send.failed": "Failed to send message", "error:message.send.failed": "Failed to send message",
"error:message.react.failed": "Failed to add reaction", "error:message.react.failed": "Failed to add reaction",
"info.sending.message": "Sending...", "info.sending.message": "Sending...",
"larp.redacted": "This message was deleted." "larp.redacted": "This message was deleted.",
"placeholder.invitation.code": "invitation code",
"info.messages.loading.older": "Loading older messages...",
"error:message.not.found": "Message not found"
} }

View file

@ -1750,6 +1750,9 @@ document.addEventListener('touchmove', e => {
document.addEventListener('touchend', e => { document.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].screenX; touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY; touchEndY = e.changedTouches[0].screenY;
isAdjusting = false; //resize scroll fix
handleMobileSwipe(); handleMobileSwipe();
if (activeTouchButton) { if (activeTouchButton) {
@ -1985,7 +1988,7 @@ async function renderMessages(messages, isPrepend = false) {
if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes); if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
decrypted = true; decrypted = true;
} catch (e) { } catch (e) {
content = `<span style="color:var(--error-color)"><blah>error:messages.decrypt.failed</blah></span>`; content = `<span style="color:var(--big-red)"><blah>error:messages.decrypt.failed</blah></span>`;
decrypted = false; decrypted = false;
} }
} else { } else {
@ -2145,16 +2148,20 @@ async function renderMessages(messages, isPrepend = false) {
} }
} }
let isRedacted = msg.type === "larp.redacted";
let redactedStyle = isRedacted ? ` opacity: 0.5; font-style: italic;` : ``;
let redactedIcon = isRedacted ? `<svg xmlns="http://www.w3.org/2000/svg" height="1rem" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: -0.165em; margin-right: 0.2rem;"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>` : ``;
html += ` html += `
<div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}"> <div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`} ${showAvatar ? `<img src="${pfp}" class="chat-message-pfp">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')" ontouchstart="handleMessageTouchStart(event, '${msgId}')" ontouchend="handleMessageTouchEnd()" ontouchmove="handleMessageTouchMove()"> <div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')">
${showAvatar ? `<div class="chat-message-header"> ${showAvatar ? `<div class="chat-message-header">
<span class="chat-message-author">${authorName}</span> <span class="chat-message-author">${authorName}</span>
<span class="chat-message-timestamp">${timeStr}</span> <span class="chat-message-timestamp">${timeStr}</span>
</div>` : ""} </div>` : ""}
${repliedHtml} ${repliedHtml}
<div class="chat-message-text">${content}</div> <div class="chat-message-text" style="${redactedStyle}">${redactedIcon}${content}</div>
${reactionsHtml} ${reactionsHtml}
</div> </div>
</div> </div>
@ -2197,7 +2204,7 @@ window.visualViewport?.addEventListener("resize", async () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
ignoreScroll = false; ignoreScroll = false;
}); });
await delay(10); await delay(1);
} }
}); });
function setupChatScrollListener() { function setupChatScrollListener() {
@ -2277,6 +2284,23 @@ async function sendMessage() {
showBlahNotification(res); showBlahNotification(res);
let pElem = document.getElementById(pendingMsgId); let pElem = document.getElementById(pendingMsgId);
if (pElem) pElem.remove(); if (pElem) pElem.remove();
} else if (res && res.startsWith("success:")) {
let parts = res.split(":");
if (parts.length > 1 && parts[1] !== "message.sent") {
let newMsgId = parts[1];
loadedMessages[newMsgId] = {
author: id,
timestamp: Date.now().toString(),
type: "larp.text",
content: encryptedContent,
attachment: "",
key: "0",
pervious: "",
responded: replyingToMsgId || "",
reactions: ""
};
renderMessages(loadedMessages);
}
} }
} catch (e) { } catch (e) {
clearAction("msgsending"); clearAction("msgsending");
@ -2309,20 +2333,28 @@ function handleChatInputKey(event) {
let dmMessagePollInterval = null; let dmMessagePollInterval = null;
let appWebSocket = null; let appWebSocket = null;
let dmMessageLoadTimeout = null;
function setupWebSocket() { function setupWebSocket() {
if (appWebSocket && appWebSocket.readyState === WebSocket.OPEN) return; if (appWebSocket && (appWebSocket.readyState === WebSocket.OPEN || appWebSocket.readyState === WebSocket.CONNECTING)) return;
let wsUrl = url.replace(/^http/, "ws") + "/ws"; let wsUrl = url.replace(/^http/, "ws") + "/ws";
appWebSocket = new WebSocket(wsUrl); appWebSocket = new WebSocket(wsUrl);
appWebSocket.onopen = async () => { appWebSocket.onopen = async () => {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try { try {
let nonce = await getNonce(id, passwordHash); let nonce = await getNonce(id, passwordHash);
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce); let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc})); appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
} catch (e) { } catch (e) {
console.error("WS auth failed", e); console.error("WS auth failed", e);
} finally {
release();
} }
}; };
@ -2331,7 +2363,10 @@ function setupWebSocket() {
if (data.startsWith("dm_message:")) { if (data.startsWith("dm_message:")) {
let msgDmId = data.substring("dm_message:".length); let msgDmId = data.substring("dm_message:".length);
if (currentDmId === msgDmId) { if (currentDmId === msgDmId) {
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
dmMessageLoadTimeout = setTimeout(() => {
loadDmMessages(msgDmId); loadDmMessages(msgDmId);
}, 300);
} }
} }
}; };
@ -2342,11 +2377,14 @@ function setupWebSocket() {
} }
let currentContextMenuMsgId = null; let currentContextMenuMsgId = null;
let touchTimeout = null; let touchContextMenuFired = false;
let touchTarget = null;
function handleMessageContextMenu(e, msgId) { function handleMessageContextMenu(e, msgId) {
e.preventDefault(); e.preventDefault();
if (touchContextMenuFired) {
touchContextMenuFired = false;
return;
}
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles(); if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
currentContextMenuMsgId = msgId; currentContextMenuMsgId = msgId;
let elem = document.getElementById(`msg-${msgId}`); let elem = document.getElementById(`msg-${msgId}`);
@ -2363,40 +2401,6 @@ function handleMessageContextMenu(e, msgId) {
} }
} }
function handleMessageTouchStart(e, msgId) {
touchTarget = e.target;
touchTimeout = setTimeout(() => {
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
currentContextMenuMsgId = msgId;
let elem = document.getElementById(`msg-${msgId}`);
if (elem) elem.classList.add("context-menu-open");
let touch = e.touches[0];
showFixedContextMenu({
top: touch.clientY,
right: touch.clientX,
bottom: touch.clientY,
left: touch.clientX
}, messageContextMenu);
let delBtn = fixedContextMenu.querySelector("#context-delete-btn");
if (delBtn) {
if (loadedMessages[msgId] && loadedMessages[msgId].author !== id) {
delBtn.style.display = "none";
} else {
delBtn.style.display = "";
}
}
}, 500);
}
function handleMessageTouchEnd() {
if (touchTimeout) clearTimeout(touchTimeout);
}
function handleMessageTouchMove() {
if (touchTimeout) clearTimeout(touchTimeout);
}
async function deleteMessage(msgId) { async function deleteMessage(msgId) {
if (fixedContextMenu) fixedContextMenu.classList.remove("show"); if (fixedContextMenu) fixedContextMenu.classList.remove("show");
showAction("action.message.deleting", "msgdel"); showAction("action.message.deleting", "msgdel");
@ -2535,6 +2539,11 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes); let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
if (currentMsg) {
currentMsg.reactions = encryptedReactions;
renderMessages(loadedMessages);
}
let msgPayload = { let msgPayload = {
string1: currentDmId, string1: currentDmId,
string2: msgId, string2: msgId,

View file

@ -133,9 +133,9 @@ var chatScreen = `
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;"> <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;"> <div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
</div> </div>
<div id="replying-bar" style="display: none; padding: 0.5rem 1rem; border-top: var(--border-width) solid var(--light-border-color); font-size: 0.85rem; justify-content: space-between; align-items: center; background: var(--main-bg-color);"> <div id="replying-bar" style="display: none; padding: 0 1rem; border-top: var(--border-width) solid var(--light-border-color); font-size: 0.85rem; justify-content: space-between; align-items: center; background: var(--main-bg-color);">
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;"> <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;">
<span style="opacity: 0.7;"><blah>title.replied.to</blah> </span><strong id="replying-to-name"></strong>: <span id="replying-to-text" style="opacity: 0.7;"></span> <span style="opacity: 0.7;"><blah>title.replying.to</blah> </span><strong id="replying-to-name"></strong>: <span id="replying-to-text" style="opacity: 0.7;"></span>
</div> </div>
<button onclick="cancelReply()" style="background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0.2rem; margin-left: 0.5rem;"> <button onclick="cancelReply()" style="background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0.2rem; margin-left: 0.5rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" viewBox="0 -960 960 960" fill="currentColor"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" viewBox="0 -960 960 960" fill="currentColor"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>

View file

@ -159,6 +159,11 @@ indicator.active {
border-radius: 0.65rem; border-radius: 0.65rem;
} }
.reaction-pill {
backface-visibility: hidden;
transform: translateZ(0);
}
roomcontent { roomcontent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;