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",
|
"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"
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
loadDmMessages(msgDmId);
|
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
|
||||||
|
dmMessageLoadTimeout = setTimeout(() => {
|
||||||
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
loadDmMessages(msgDmId);
|
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
|
||||||
|
dmMessageLoadTimeout = setTimeout(() => {
|
||||||
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue