diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json
index bfd18b02..8051a7fb 100644
--- a/android/app/src/main/assets/public/blah/en-cat.json
+++ b/android/app/src/main/assets/public/blah/en-cat.json
@@ -147,8 +147,10 @@
"error:message.react.failed": "failed to add purr",
"info.sending.message": "sending...",
"placeholder.message.input": "meow...",
- "larp.redacted": "this meow was hissed away.",
- "placeholder.invitation.code": "invitation code meow",
+ "larp.redacted": "Meow meow was meowed.",
+ "message.already.deleted": "This meow has already been meowed.",
+ "rate.limit": "You are meowing too quickly. Please meow again later.",
+ "placeholder.invitation.code": "meow meow meow",
"info.messages.loading.older": "Loading older meowsages...",
"error:message.not.found": "Meowsage not found",
"title.profile": "cat profile",
diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json
index 3aebb7f2..4d5c9155 100644
--- a/android/app/src/main/assets/public/blah/en-us.json
+++ b/android/app/src/main/assets/public/blah/en-us.json
@@ -145,6 +145,8 @@
"error:message.react.failed": "Failed to add reaction",
"info.sending.message": "Sending...",
"larp.redacted": "This message was deleted.",
+ "message.already.deleted": "This message has already been deleted.",
+ "rate.limit": "You are sending requests too quickly. Please try again later.",
"placeholder.invitation.code": "invitation code",
"info.messages.loading.older": "Loading older messages...",
"error:message.not.found": "Message not found",
diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html
index dd5bc391..a5daae41 100644
--- a/android/app/src/main/assets/public/index.html
+++ b/android/app/src/main/assets/public/index.html
@@ -157,9 +157,9 @@
}
}
- async function refreshDms(waittime = 0) {
+ async function refreshDms(waittime = 0, showLoading = true) {
try {
- showAction("action.dm.fetch", "dmrefresh");
+ if (showLoading) showAction("action.dm.fetch", "dmrefresh");
await delay(waittime);
if (window.cachedDms && typeof renderDms === 'function') {
await renderDms(window.cachedDms);
@@ -169,10 +169,10 @@
if (typeof renderDms === 'function') {
await renderDms(res);
}
- clearAction("dmrefresh");
+ if (showLoading) clearAction("dmrefresh");
}
catch (e) {
- clearAction("dmrefresh");
+ if (showLoading) clearAction("dmrefresh");
showBlahNotification("error:dm.refresh.failed");
}
}
diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js
index ce9aae5c..8e5e81d0 100644
--- a/android/app/src/main/assets/public/main.js
+++ b/android/app/src/main/assets/public/main.js
@@ -1533,6 +1533,13 @@ async function openProfile(accountId) {
try {
if (!accountId) accountId = window.id; //var fallback
+
+ if (accountId.endsWith(';' + host)) {
+ accountId = accountId.substring(0, accountId.length - host.length - 1);
+ } else if (accountId.endsWith(':' + host)) {
+ accountId = accountId.substring(0, accountId.length - host.length - 1);
+ }
+
let nameWithHost = await fetchAsync(`${url}/idtoname?id=${accountId}`);
if (nameWithHost.startsWith("error:")) nameWithHost = accountId; //api fallback
@@ -1632,7 +1639,7 @@ async function openProfile(accountId) {
${profileName}
-
${profileName}:${profileHost} (${accountId.split(':')[0]})
+
${profileName}:${profileHost} (${accountId.split(':')[0].split(';')[0]})
${bioHtml}
${actionBtnHtml}
@@ -2098,11 +2105,14 @@ async function renderDms(res) {
let pfp = await getAvatarUrl(targetId, targetUsername);
let displayName = targetUsername.split(':')[0];
+ let isRead = dmData.string3 === "true";
+ let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;";
+
html += `
@@ -2188,6 +2198,8 @@ async function openDm(dmId, username, targetId) {
try {
showAction("action.dm.opening", "dmopen");
+ markDmAsRead(dmId);
+
currentDmKey = await ensureDmRoomKey(dmId);
if (!currentDmKey) {
throw new Error("Missing DM room key");
@@ -2484,13 +2496,21 @@ async function renderMessages(messages, isPrepend = false) {
container.innerHTML = html;
+ let targetScrollTop = 0;
if (wasAtBottom) {
- container.scrollTop = container.scrollHeight;
+ targetScrollTop = container.scrollHeight;
} else if (isPrepend) {
- container.scrollTop = oldScrollTop + (container.scrollHeight - oldScrollHeight);
+ targetScrollTop = oldScrollTop + (container.scrollHeight - oldScrollHeight);
} else {
- container.scrollTop = oldScrollTop;
+ targetScrollTop = oldScrollTop;
}
+
+ ignoreScroll = true;
+ container.scrollTop = targetScrollTop;
+ lastChatScroll = container.scrollHeight - targetScrollTop - container.clientHeight;
+ if (lastChatScroll < 0) lastChatScroll = 0;
+
+ setTimeout(() => { ignoreScroll = false; }, 50);
}
var lastChatScroll = 0;
@@ -2528,6 +2548,9 @@ function setupChatScrollListener() {
if (!ignoreScroll) {
isAdjusting = false;
lastChatScroll = container.scrollHeight - container.scrollTop - container.clientHeight;
+ if (lastChatScroll <= 10) {
+ handleDmInteraction();
+ }
}
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
@@ -2678,7 +2701,18 @@ function setupWebSocket() {
appWebSocket.onmessage = (event) => {
let data = event.data;
if (data.startsWith("dm_message:")) {
- let msgDmId = data.substring("dm_message:".length);
+ let parts = data.split(":");
+ let msgDmId = parts[1];
+ let msgType = parts[2] || "unread";
+
+ if (msgType === "unread") {
+ updateDmListUI(msgDmId, false, Date.now().toString());
+ } else if (msgType === "read") {
+ updateDmListUI(msgDmId, true, Date.now().toString());
+ } else if (msgType === "update") {
+ // do nothing to UI sorting/read state
+ }
+
if (currentDmId === msgDmId) {
if (dmMessageLoadTimeout) clearTimeout(dmMessageLoadTimeout);
dmMessageLoadTimeout = setTimeout(() => {
@@ -2709,12 +2743,21 @@ function handleMessageContextMenu(e, msgId) {
showFixedContextMenu({top: e.clientY, right: e.clientX, bottom: e.clientY, left: e.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 = "";
+ let reactBtn = fixedContextMenu.querySelector("#context-react-btn");
+ let msg = loadedMessages[msgId];
+
+ if (msg && msg.type === "larp.redacted") {
+ if (delBtn) delBtn.style.display = "none";
+ if (reactBtn) reactBtn.style.display = "none";
+ } else {
+ if (delBtn) {
+ if (msg && msg.author !== id) {
+ delBtn.style.display = "none";
+ } else {
+ delBtn.style.display = "";
+ }
}
+ if (reactBtn) reactBtn.style.display = "";
}
}
@@ -2895,6 +2938,11 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
showAction("action.message.reacting", "msgreact");
try {
let currentMsg = loadedMessages[msgId];
+ if (currentMsg && currentMsg.type === "larp.redacted") {
+ clearAction("msgreact");
+ showNotification(processBlah("error.message.already.deleted"), "error");
+ return;
+ }
let existingReactions = [];
if (currentMsg && currentMsg.reactions) {
let rxStr = currentMsg.reactions;
@@ -2922,7 +2970,9 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
+ let originalReactionsEncrypted = "";
if (currentMsg) {
+ originalReactionsEncrypted = currentMsg.reactions || "";
currentMsg.reactions = encryptedReactions;
renderMessages(loadedMessages);
}
@@ -2935,6 +2985,10 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
let res = await fetchEncrypted("dm/message/react", JSON.stringify(msgPayload));
if (res && res.startsWith("error:")) {
showBlahNotification(res);
+ if (currentMsg) {
+ currentMsg.reactions = originalReactionsEncrypted;
+ renderMessages(loadedMessages);
+ }
}
} catch (e) {
showBlahNotification("error:message.react.failed");
@@ -3430,4 +3484,69 @@ if (container) {
document.addEventListener('touchmove', onDrag, {passive: true});
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
-}
\ No newline at end of file
+}
+
+function updateDmListUI(dmId, isRead, timestamp = null) {
+ try {
+ if (!window.cachedDms) return;
+ let parsedDms = JSON.parse(window.cachedDms);
+ if (!parsedDms.dms || !parsedDms.dms[dmId]) return;
+
+ let changed = false;
+ if (parsedDms.dms[dmId].string3 !== (isRead ? "true" : "false")) {
+ parsedDms.dms[dmId].string3 = isRead ? "true" : "false";
+ changed = true;
+ }
+ if (timestamp && parsedDms.dms[dmId].string2 !== timestamp) {
+ parsedDms.dms[dmId].string2 = timestamp;
+ changed = true;
+ }
+
+ if (changed) {
+ window.cachedDms = JSON.stringify(parsedDms);
+ }
+
+ let btn = document.getElementById(`dm-btn-${dmId}`);
+ let container = document.getElementById("dms-list");
+ if (btn && container) {
+ let nameSpan = btn.querySelector('.room-name');
+ if (nameSpan) {
+ nameSpan.style.opacity = isRead ? "0.8" : "1";
+ }
+ if (timestamp) {
+ container.prepend(btn);
+ }
+ } else {
+ if (typeof refreshDms === 'function') refreshDms(0, false);
+ }
+ } catch (e) {
+ console.error(e);
+ if (typeof refreshDms === 'function') refreshDms(0, false);
+ }
+}
+
+function markDmAsRead(dmId) {
+ fetchEncrypted("dm/read", JSON.stringify({string1: dmId})).catch(()=>{});
+ updateDmListUI(dmId, true);
+}
+
+function handleDmInteraction() {
+ if (!currentDmId) return;
+ let container = document.getElementById("chat-messages");
+ if (!container) return;
+
+ if (lastChatScroll <= 10) {
+ try {
+ if (window.cachedDms) {
+ let parsedDms = JSON.parse(window.cachedDms);
+ if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") {
+ markDmAsRead(currentDmId);
+ }
+ }
+ } catch (e) {}
+ }
+}
+
+document.addEventListener('click', handleDmInteraction);
+document.addEventListener('keydown', handleDmInteraction);
+document.addEventListener('touchstart', handleDmInteraction);
\ No newline at end of file
diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js
index 114c18d8..641fac88 100644
--- a/android/app/src/main/assets/public/screens.js
+++ b/android/app/src/main/assets/public/screens.js
@@ -245,7 +245,7 @@ var messageContextMenu = `
title.reply.message
-