Fixed context menu flicker, resize scrolling and race conditions
This commit is contained in:
parent
4016975900
commit
3f0f628569
10 changed files with 140 additions and 118 deletions
|
|
@ -45,15 +45,12 @@
|
|||
"password.cant.empty": "meow word cannot be empty",
|
||||
"username.cant.empty": "cat name cannot be empty",
|
||||
"bad.request": "bad request!!!",
|
||||
|
||||
"letters": "letters",
|
||||
"numbers": "numbers",
|
||||
"underscores": "underscores",
|
||||
|
||||
"loading.connecting": "connecting...",
|
||||
"loading.loading": "loading...",
|
||||
"loading.done": "ready! :3",
|
||||
|
||||
"title.home": "bed",
|
||||
"title.dms": "direct meowchats",
|
||||
"title.groups": "clowder",
|
||||
|
|
@ -80,7 +77,6 @@
|
|||
"title.sent": "sent",
|
||||
"title.all": "all",
|
||||
"title.unread": "unread",
|
||||
|
||||
"desc.no.invites": "no invites found :c",
|
||||
"desc.no.notifications": "no notifications found :c",
|
||||
"desc.fetching.invites": "fetching invites...",
|
||||
|
|
@ -89,7 +85,6 @@
|
|||
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
|
||||
"desc.invite.group.received": "{0} ({1}) invited you to a clowder",
|
||||
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
|
||||
|
||||
"action.fetching.invites.sent": "fetching sent invites...",
|
||||
"action.fetching.invites.recv": "fetching received invites...",
|
||||
"action.dm.fetch": "fetching direct meowchats...",
|
||||
|
|
@ -98,7 +93,6 @@
|
|||
"action.invite.revoking": "revoking invite...",
|
||||
"action.invite.accepting": "accepting invite...",
|
||||
"action.invite.declining": "declining invite...",
|
||||
|
||||
"title.sign.up": "sign up",
|
||||
"title.sign.in": "sign in",
|
||||
"title.sign.in.to": "sign in to your larpix instance",
|
||||
|
|
@ -132,6 +126,7 @@
|
|||
"title.react.message": "purr",
|
||||
"title.delete.message": "hiss away",
|
||||
"title.replied.to": "meowed back to",
|
||||
"title.replying.to": "meowing back to",
|
||||
"action.message.deleting": "hissing message away...",
|
||||
"action.message.sending": "sending meow...",
|
||||
"action.message.reacting": "adding purr...",
|
||||
|
|
@ -140,5 +135,8 @@
|
|||
"error:message.react.failed": "failed to add purr",
|
||||
"info.sending.message": "sending...",
|
||||
"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"
|
||||
}
|
||||
|
|
@ -45,15 +45,12 @@
|
|||
"password.cant.empty": "Password cannot be empty",
|
||||
"username.cant.empty": "Username cannot be empty",
|
||||
"bad.request": "Bad request",
|
||||
|
||||
"letters": "letters",
|
||||
"numbers": "numbers",
|
||||
"underscores": "underscores",
|
||||
|
||||
"loading.connecting": "Connecting...",
|
||||
"loading.loading": "Loading...",
|
||||
"loading.done": "Ready!",
|
||||
|
||||
"title.home": "Home",
|
||||
"title.dms": "Direct messages",
|
||||
"title.groups": "Groups",
|
||||
|
|
@ -88,7 +85,6 @@
|
|||
"desc.invite.dm.sent": "You invited {0} ({1}) to chat",
|
||||
"desc.invite.group.received": "{0} ({1}) invited you to a group",
|
||||
"desc.invite.group.sent": "You invited {0} ({1}) to a group",
|
||||
|
||||
"action.fetching.invites.sent": "Fetching sent invites...",
|
||||
"action.fetching.invites.recv": "Fetching received invites...",
|
||||
"action.dm.fetch": "Fetching dms...",
|
||||
|
|
@ -97,7 +93,6 @@
|
|||
"action.invite.revoking": "Revoking invite...",
|
||||
"action.invite.accepting": "Accepting invite...",
|
||||
"action.invite.declining": "Declining invite...",
|
||||
|
||||
"title.sign.up": "Sign Up",
|
||||
"title.sign.in": "Sign In",
|
||||
"title.sign.in.to": "Sign in to your larpix instance",
|
||||
|
|
@ -132,6 +127,7 @@
|
|||
"title.react.message": "React",
|
||||
"title.delete.message": "Delete",
|
||||
"title.replied.to": "Replied to",
|
||||
"title.replying.to": "Replying to",
|
||||
"action.message.deleting": "Deleting message...",
|
||||
"action.message.sending": "Sending message...",
|
||||
"action.message.reacting": "Adding reaction...",
|
||||
|
|
@ -139,5 +135,8 @@
|
|||
"error:message.send.failed": "Failed to send message",
|
||||
"error:message.react.failed": "Failed to add reaction",
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1750,6 +1750,9 @@ document.addEventListener('touchmove', e => {
|
|||
document.addEventListener('touchend', e => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
touchEndY = e.changedTouches[0].screenY;
|
||||
|
||||
isAdjusting = false; //resize scroll fix
|
||||
|
||||
handleMobileSwipe();
|
||||
|
||||
if (activeTouchButton) {
|
||||
|
|
@ -1985,7 +1988,7 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
|
||||
decrypted = true;
|
||||
} 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;
|
||||
}
|
||||
} 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 += `
|
||||
<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>`}
|
||||
<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">
|
||||
<span class="chat-message-author">${authorName}</span>
|
||||
<span class="chat-message-timestamp">${timeStr}</span>
|
||||
</div>` : ""}
|
||||
${repliedHtml}
|
||||
<div class="chat-message-text">${content}</div>
|
||||
<div class="chat-message-text" style="${redactedStyle}">${redactedIcon}${content}</div>
|
||||
${reactionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2197,7 +2204,7 @@ window.visualViewport?.addEventListener("resize", async () => {
|
|||
requestAnimationFrame(() => {
|
||||
ignoreScroll = false;
|
||||
});
|
||||
await delay(10);
|
||||
await delay(1);
|
||||
}
|
||||
});
|
||||
function setupChatScrollListener() {
|
||||
|
|
@ -2277,6 +2284,23 @@ async function sendMessage() {
|
|||
showBlahNotification(res);
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
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) {
|
||||
clearAction("msgsending");
|
||||
|
|
@ -2309,20 +2333,28 @@ function handleChatInputKey(event) {
|
|||
|
||||
let dmMessagePollInterval = null;
|
||||
let appWebSocket = null;
|
||||
let dmMessageLoadTimeout = null;
|
||||
|
||||
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";
|
||||
appWebSocket = new WebSocket(wsUrl);
|
||||
|
||||
appWebSocket.onopen = async () => {
|
||||
let release;
|
||||
const lock = new Promise(resolve => release = resolve);
|
||||
const prevMutex = requestMutex;
|
||||
requestMutex = prevMutex.then(() => lock);
|
||||
await prevMutex;
|
||||
try {
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
|
||||
appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
|
||||
} catch (e) {
|
||||
console.error("WS auth failed", e);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2331,7 +2363,10 @@ function setupWebSocket() {
|
|||
if (data.startsWith("dm_message:")) {
|
||||
let msgDmId = data.substring("dm_message:".length);
|
||||
if (currentDmId === msgDmId) {
|
||||
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
|
||||
dmMessageLoadTimeout = setTimeout(() => {
|
||||
loadDmMessages(msgDmId);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -2342,11 +2377,14 @@ function setupWebSocket() {
|
|||
}
|
||||
|
||||
let currentContextMenuMsgId = null;
|
||||
let touchTimeout = null;
|
||||
let touchTarget = null;
|
||||
let touchContextMenuFired = false;
|
||||
|
||||
function handleMessageContextMenu(e, msgId) {
|
||||
e.preventDefault();
|
||||
if (touchContextMenuFired) {
|
||||
touchContextMenuFired = false;
|
||||
return;
|
||||
}
|
||||
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||
currentContextMenuMsgId = 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) {
|
||||
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||
showAction("action.message.deleting", "msgdel");
|
||||
|
|
@ -2535,6 +2539,11 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
|
||||
|
||||
if (currentMsg) {
|
||||
currentMsg.reactions = encryptedReactions;
|
||||
renderMessages(loadedMessages);
|
||||
}
|
||||
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId,
|
||||
|
|
|
|||
|
|
@ -133,9 +133,9 @@ var chatScreen = `
|
|||
<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>
|
||||
<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;">
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ indicator.active {
|
|||
border-radius: 0.65rem;
|
||||
}
|
||||
|
||||
.reaction-pill {
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
roomcontent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -45,15 +45,12 @@
|
|||
"password.cant.empty": "meow word cannot be empty",
|
||||
"username.cant.empty": "cat name cannot be empty",
|
||||
"bad.request": "bad request!!!",
|
||||
|
||||
"letters": "letters",
|
||||
"numbers": "numbers",
|
||||
"underscores": "underscores",
|
||||
|
||||
"loading.connecting": "connecting...",
|
||||
"loading.loading": "loading...",
|
||||
"loading.done": "ready! :3",
|
||||
|
||||
"title.home": "bed",
|
||||
"title.dms": "direct meowchats",
|
||||
"title.groups": "clowder",
|
||||
|
|
@ -80,7 +77,6 @@
|
|||
"title.sent": "sent",
|
||||
"title.all": "all",
|
||||
"title.unread": "unread",
|
||||
|
||||
"desc.no.invites": "no invites found :c",
|
||||
"desc.no.notifications": "no notifications found :c",
|
||||
"desc.fetching.invites": "fetching invites...",
|
||||
|
|
@ -89,7 +85,6 @@
|
|||
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
|
||||
"desc.invite.group.received": "{0} ({1}) invited you to a clowder",
|
||||
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
|
||||
|
||||
"action.fetching.invites.sent": "fetching sent invites...",
|
||||
"action.fetching.invites.recv": "fetching received invites...",
|
||||
"action.dm.fetch": "fetching direct meowchats...",
|
||||
|
|
@ -98,7 +93,6 @@
|
|||
"action.invite.revoking": "revoking invite...",
|
||||
"action.invite.accepting": "accepting invite...",
|
||||
"action.invite.declining": "declining invite...",
|
||||
|
||||
"title.sign.up": "sign up",
|
||||
"title.sign.in": "sign in",
|
||||
"title.sign.in.to": "sign in to your larpix instance",
|
||||
|
|
@ -132,6 +126,7 @@
|
|||
"title.react.message": "purr",
|
||||
"title.delete.message": "hiss away",
|
||||
"title.replied.to": "meowed back to",
|
||||
"title.replying.to": "meowing back to",
|
||||
"action.message.deleting": "hissing message away...",
|
||||
"action.message.sending": "sending meow...",
|
||||
"action.message.reacting": "adding purr...",
|
||||
|
|
@ -140,5 +135,8 @@
|
|||
"error:message.react.failed": "failed to add purr",
|
||||
"info.sending.message": "sending...",
|
||||
"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"
|
||||
}
|
||||
|
|
@ -45,15 +45,12 @@
|
|||
"password.cant.empty": "Password cannot be empty",
|
||||
"username.cant.empty": "Username cannot be empty",
|
||||
"bad.request": "Bad request",
|
||||
|
||||
"letters": "letters",
|
||||
"numbers": "numbers",
|
||||
"underscores": "underscores",
|
||||
|
||||
"loading.connecting": "Connecting...",
|
||||
"loading.loading": "Loading...",
|
||||
"loading.done": "Ready!",
|
||||
|
||||
"title.home": "Home",
|
||||
"title.dms": "Direct messages",
|
||||
"title.groups": "Groups",
|
||||
|
|
@ -88,7 +85,6 @@
|
|||
"desc.invite.dm.sent": "You invited {0} ({1}) to chat",
|
||||
"desc.invite.group.received": "{0} ({1}) invited you to a group",
|
||||
"desc.invite.group.sent": "You invited {0} ({1}) to a group",
|
||||
|
||||
"action.fetching.invites.sent": "Fetching sent invites...",
|
||||
"action.fetching.invites.recv": "Fetching received invites...",
|
||||
"action.dm.fetch": "Fetching dms...",
|
||||
|
|
@ -97,7 +93,6 @@
|
|||
"action.invite.revoking": "Revoking invite...",
|
||||
"action.invite.accepting": "Accepting invite...",
|
||||
"action.invite.declining": "Declining invite...",
|
||||
|
||||
"title.sign.up": "Sign Up",
|
||||
"title.sign.in": "Sign In",
|
||||
"title.sign.in.to": "Sign in to your larpix instance",
|
||||
|
|
@ -132,6 +127,7 @@
|
|||
"title.react.message": "React",
|
||||
"title.delete.message": "Delete",
|
||||
"title.replied.to": "Replied to",
|
||||
"title.replying.to": "Replying to",
|
||||
"action.message.deleting": "Deleting message...",
|
||||
"action.message.sending": "Sending message...",
|
||||
"action.message.reacting": "Adding reaction...",
|
||||
|
|
@ -139,5 +135,8 @@
|
|||
"error:message.send.failed": "Failed to send message",
|
||||
"error:message.react.failed": "Failed to add reaction",
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1750,6 +1750,9 @@ document.addEventListener('touchmove', e => {
|
|||
document.addEventListener('touchend', e => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
touchEndY = e.changedTouches[0].screenY;
|
||||
|
||||
isAdjusting = false; //resize scroll fix
|
||||
|
||||
handleMobileSwipe();
|
||||
|
||||
if (activeTouchButton) {
|
||||
|
|
@ -1985,7 +1988,7 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
if (reactions) reactions = await decryptAesGcmFromBase64(reactions, dmKeyBytes);
|
||||
decrypted = true;
|
||||
} 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;
|
||||
}
|
||||
} 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 += `
|
||||
<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>`}
|
||||
<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">
|
||||
<span class="chat-message-author">${authorName}</span>
|
||||
<span class="chat-message-timestamp">${timeStr}</span>
|
||||
</div>` : ""}
|
||||
${repliedHtml}
|
||||
<div class="chat-message-text">${content}</div>
|
||||
<div class="chat-message-text" style="${redactedStyle}">${redactedIcon}${content}</div>
|
||||
${reactionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2197,7 +2204,7 @@ window.visualViewport?.addEventListener("resize", async () => {
|
|||
requestAnimationFrame(() => {
|
||||
ignoreScroll = false;
|
||||
});
|
||||
await delay(10);
|
||||
await delay(1);
|
||||
}
|
||||
});
|
||||
function setupChatScrollListener() {
|
||||
|
|
@ -2277,6 +2284,23 @@ async function sendMessage() {
|
|||
showBlahNotification(res);
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
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) {
|
||||
clearAction("msgsending");
|
||||
|
|
@ -2309,20 +2333,28 @@ function handleChatInputKey(event) {
|
|||
|
||||
let dmMessagePollInterval = null;
|
||||
let appWebSocket = null;
|
||||
let dmMessageLoadTimeout = null;
|
||||
|
||||
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";
|
||||
appWebSocket = new WebSocket(wsUrl);
|
||||
|
||||
appWebSocket.onopen = async () => {
|
||||
let release;
|
||||
const lock = new Promise(resolve => release = resolve);
|
||||
const prevMutex = requestMutex;
|
||||
requestMutex = prevMutex.then(() => lock);
|
||||
await prevMutex;
|
||||
try {
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
let secretEnc = await encryptWithNonce(passwordHash, passwordHash, nonce);
|
||||
appWebSocket.send(JSON.stringify({string1: id, string2: secretEnc}));
|
||||
} catch (e) {
|
||||
console.error("WS auth failed", e);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2331,7 +2363,10 @@ function setupWebSocket() {
|
|||
if (data.startsWith("dm_message:")) {
|
||||
let msgDmId = data.substring("dm_message:".length);
|
||||
if (currentDmId === msgDmId) {
|
||||
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
|
||||
dmMessageLoadTimeout = setTimeout(() => {
|
||||
loadDmMessages(msgDmId);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -2342,11 +2377,14 @@ function setupWebSocket() {
|
|||
}
|
||||
|
||||
let currentContextMenuMsgId = null;
|
||||
let touchTimeout = null;
|
||||
let touchTarget = null;
|
||||
let touchContextMenuFired = false;
|
||||
|
||||
function handleMessageContextMenu(e, msgId) {
|
||||
e.preventDefault();
|
||||
if (touchContextMenuFired) {
|
||||
touchContextMenuFired = false;
|
||||
return;
|
||||
}
|
||||
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||
currentContextMenuMsgId = 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) {
|
||||
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||
showAction("action.message.deleting", "msgdel");
|
||||
|
|
@ -2535,6 +2539,11 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
|
||||
|
||||
if (currentMsg) {
|
||||
currentMsg.reactions = encryptedReactions;
|
||||
renderMessages(loadedMessages);
|
||||
}
|
||||
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId,
|
||||
|
|
|
|||
|
|
@ -133,9 +133,9 @@ var chatScreen = `
|
|||
<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>
|
||||
<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;">
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ indicator.active {
|
|||
border-radius: 0.65rem;
|
||||
}
|
||||
|
||||
.reaction-pill {
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
roomcontent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue