Changes:
- add replies - add reactions - add message redacting - fix chat height when opening mobile keyboard
This commit is contained in:
parent
75cd78ed5a
commit
4016975900
10 changed files with 1286 additions and 310 deletions
|
|
@ -120,7 +120,6 @@
|
|||
"title.back.to.register": "back to registration",
|
||||
"placeholder.username": "cat name",
|
||||
"placeholder.captcha.code": "captcha code",
|
||||
"placeholder.message.input": "meow...",
|
||||
"desc.messages.loading": "loading meows...",
|
||||
"desc.no.dms": "no direct meowchats :c",
|
||||
"action.dm.opening": "opening meowchat...",
|
||||
|
|
@ -128,5 +127,18 @@
|
|||
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
|
||||
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
|
||||
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
||||
"messages.decrypt.failed": "failed to decrypt meow :c"
|
||||
"messages.decrypt.failed": "failed to decrypt meow :c",
|
||||
"title.reply.message": "meow back",
|
||||
"title.react.message": "purr",
|
||||
"title.delete.message": "hiss away",
|
||||
"title.replied.to": "meowed back to",
|
||||
"action.message.deleting": "hissing message away...",
|
||||
"action.message.sending": "sending meow...",
|
||||
"action.message.reacting": "adding purr...",
|
||||
"error:message.delete.failed": "failed to hiss away meow",
|
||||
"error:message.send.failed": "failed to send meow",
|
||||
"error:message.react.failed": "failed to add purr",
|
||||
"info.sending.message": "sending...",
|
||||
"placeholder.message.input": "meow...",
|
||||
"larp.redacted": "this meow was hissed away."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,5 +127,17 @@
|
|||
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
|
||||
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
|
||||
"dm.messages.fetch.failed": "Failed to fetch messages",
|
||||
"messages.decrypt.failed": "Failed to decrypt message"
|
||||
"messages.decrypt.failed": "Failed to decrypt message",
|
||||
"title.reply.message": "Reply",
|
||||
"title.react.message": "React",
|
||||
"title.delete.message": "Delete",
|
||||
"title.replied.to": "Replied to",
|
||||
"action.message.deleting": "Deleting message...",
|
||||
"action.message.sending": "Sending message...",
|
||||
"action.message.reacting": "Adding reaction...",
|
||||
"error:message.delete.failed": "Failed to delete message",
|
||||
"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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
var prot = window.location.protocol;
|
||||
|
||||
async function updateProtocolAndUrl(host)
|
||||
{
|
||||
async function updateProtocolAndUrl(host) {
|
||||
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
||||
if (prot == "http:") {
|
||||
try {
|
||||
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
try {
|
||||
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
||||
prot = "https:";
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {}
|
||||
}
|
||||
}
|
||||
url = `${prot}//${host}/_larpix`;
|
||||
|
|
@ -744,6 +742,7 @@ function getInitials(name) {
|
|||
//1 word at least 2 letters
|
||||
return word.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function stringToColor(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
|
|
@ -755,6 +754,7 @@ function stringToColor(str) {
|
|||
|
||||
return `hsl(${h}, ${s}%, ${l}%)`;
|
||||
}
|
||||
|
||||
function createAvatarSvg(name, size = 512) {
|
||||
const initials = getInitials(name);
|
||||
const color = stringToColor(name);
|
||||
|
|
@ -777,17 +777,14 @@ function createAvatarSvg(name, size = 512) {
|
|||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
async function getAvatarUrl(id, username)
|
||||
{
|
||||
async function getAvatarUrl(id, username) {
|
||||
try {
|
||||
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
|
||||
if ((await fetchAsync(pfpUrl)) == "")
|
||||
{
|
||||
if ((await fetchAsync(pfpUrl)) == "") {
|
||||
throw Error();
|
||||
}
|
||||
return pfpUrl;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
return createAvatarSvg(username);
|
||||
}
|
||||
}
|
||||
|
|
@ -810,6 +807,7 @@ async function loadingFadeOut() {
|
|||
mainScreen.style.transform = "";
|
||||
mainScreen.style.opacity = "";
|
||||
}
|
||||
|
||||
async function loadingFadeIn() {
|
||||
|
||||
loadingScreen.style.transform = "scale(0.85)";
|
||||
|
|
@ -852,8 +850,7 @@ async function initBlahs() {
|
|||
|
||||
let placeholders = document.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -908,9 +905,7 @@ function processBlah(blahmessage) {
|
|||
message = message.replaceAll('{all}', valueslist);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
} catch (e) {
|
||||
if (prepended) {
|
||||
return blahmessage.substring(1);
|
||||
}
|
||||
|
|
@ -1082,6 +1077,7 @@ async function mainJS() {
|
|||
|
||||
await start();
|
||||
}
|
||||
|
||||
id = localStorage.getItem('id');
|
||||
username = localStorage.getItem('username');
|
||||
password = localStorage.getItem('password');
|
||||
|
|
@ -1104,8 +1100,7 @@ function showFixedContextMenu(rect, html) {
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1114,10 +1109,31 @@ function showFixedContextMenu(rect, html) {
|
|||
|
||||
fixedContextMenu.innerHTML = doc.body.innerHTML;
|
||||
|
||||
fixedContextMenu.style.left = `${rect.right + 10}px`;
|
||||
fixedContextMenu.style.top = `${rect.top}px`;
|
||||
fixedContextMenu.style.transition = 'none';
|
||||
fixedContextMenu.style.opacity = '0';
|
||||
fixedContextMenu.style.left = '0px';
|
||||
fixedContextMenu.style.top = '0px';
|
||||
|
||||
fixedContextMenu.classList.add("show");
|
||||
|
||||
let menuRect = fixedContextMenu.getBoundingClientRect();
|
||||
|
||||
let newLeft = rect.right + 10;
|
||||
let newTop = rect.top;
|
||||
|
||||
if (newLeft + menuRect.width > window.innerWidth) {
|
||||
newLeft = window.innerWidth - menuRect.width - 10;
|
||||
}
|
||||
if (newTop + menuRect.height > window.innerHeight) {
|
||||
newTop = window.innerHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
fixedContextMenu.style.left = `${newLeft}px`;
|
||||
fixedContextMenu.style.top = `${newTop}px`;
|
||||
|
||||
fixedContextMenu.offsetHeight;
|
||||
fixedContextMenu.style.transition = '';
|
||||
fixedContextMenu.style.opacity = '';
|
||||
}
|
||||
|
||||
var currentRoomsBarTitle = null;
|
||||
|
|
@ -1156,8 +1172,7 @@ async function switchRoomsBar(title, content) {
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1171,10 +1186,11 @@ async function switchRoomsBar(title, content) {
|
|||
roomsTopBarTransition.style.transform = "";
|
||||
roomsTopBarTransition.style.opacity = "";
|
||||
}
|
||||
|
||||
var currentRoomContentTitle = null;
|
||||
var currentRoomContentHtml = null;
|
||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false)
|
||||
{
|
||||
|
||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
||||
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
||||
|
|
@ -1203,9 +1219,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
|||
mainScreen.classList.remove('mobile-details');
|
||||
mainScreen.classList.add('mobile-content');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
await delay(200);
|
||||
}
|
||||
|
||||
|
|
@ -1229,8 +1243,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1246,10 +1259,11 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
|||
|
||||
|
||||
}
|
||||
|
||||
var currentDetailsContentTitle = null;
|
||||
var currentDetailsContentHtml = null;
|
||||
async function switchDetailsContent(title, content)
|
||||
{
|
||||
|
||||
async function switchDetailsContent(title, content) {
|
||||
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
|
||||
currentDetailsContentTitle = title;
|
||||
currentDetailsContentHtml = content;
|
||||
|
|
@ -1284,8 +1298,7 @@ async function switchDetailsContent(title, content)
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1301,8 +1314,8 @@ async function switchDetailsContent(title, content)
|
|||
|
||||
|
||||
}
|
||||
function clickCollapseDms()
|
||||
{
|
||||
|
||||
function clickCollapseDms() {
|
||||
var collapseDmsBtn = document.getElementById("collapse-dms");
|
||||
collapseDmsBtn.classList.toggle("collapsed");
|
||||
var dmsWrapper = document.getElementById("dms-wrapper");
|
||||
|
|
@ -1314,8 +1327,8 @@ function clickCollapseDms()
|
|||
}
|
||||
}
|
||||
}
|
||||
function clickCollapseGroups()
|
||||
{
|
||||
|
||||
function clickCollapseGroups() {
|
||||
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
||||
collapseGroupsBtn.classList.toggle("collapsed");
|
||||
var groupsWrapper = document.getElementById("groups-wrapper");
|
||||
|
|
@ -1327,12 +1340,12 @@ function clickCollapseGroups()
|
|||
}
|
||||
}
|
||||
}
|
||||
function clickAddGroup()
|
||||
{
|
||||
|
||||
function clickAddGroup() {
|
||||
switchRoomContent("title.create.group", createGroupScreen, false);
|
||||
}
|
||||
function clickAddDm()
|
||||
{
|
||||
|
||||
function clickAddDm() {
|
||||
switchRoomContent("title.create.dm", addDmScreen, false);
|
||||
}
|
||||
|
||||
|
|
@ -1353,12 +1366,20 @@ sidebarAddButton.addEventListener("click", (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
function clearContextMenuStyles() {
|
||||
let elems = document.querySelectorAll(".chat-message.context-menu-open");
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
elems[i].classList.remove("context-menu-open");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
history.pushState({trap: true}, "", location.href);
|
||||
|
||||
if (fixedContextMenu.classList.contains("show")) {
|
||||
if (!fixedContextMenu.contains(e.target)) {
|
||||
fixedContextMenu.classList.remove("show");
|
||||
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||
if (!e.target.closest('sidebarelement')) {
|
||||
setActiveSidebarIndicator(currentMainIndicator);
|
||||
}
|
||||
|
|
@ -1384,10 +1405,9 @@ if (App) {
|
|||
popstate();
|
||||
});
|
||||
}
|
||||
function popstate()
|
||||
{
|
||||
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details'))
|
||||
{
|
||||
|
||||
function popstate() {
|
||||
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
|
||||
history.back();
|
||||
if (App) {
|
||||
App.minimizeApp();
|
||||
|
|
@ -1400,6 +1420,7 @@ function popstate()
|
|||
function gotoSideProfilePopup() {
|
||||
|
||||
}
|
||||
|
||||
function setActiveRoombarItem(itemId) {
|
||||
let items = document.querySelectorAll('.collapse-text-button');
|
||||
items.forEach(item => item.classList.remove('selected'));
|
||||
|
|
@ -1529,6 +1550,7 @@ async function switchInvitesTab(tab) {
|
|||
|
||||
await loadInvites(tab);
|
||||
}
|
||||
|
||||
async function acceptInvite(targetId) { //TODO: Implement key generation
|
||||
try {
|
||||
showAction("action.invite.accepting", "invite.action");
|
||||
|
|
@ -1663,6 +1685,7 @@ function setActiveSidebarIndicator(element, isTemporary = false) {
|
|||
currentMainIndicator = element;
|
||||
}
|
||||
}
|
||||
|
||||
function mobileNavBack() {
|
||||
if (mainScreen.classList.contains('mobile-details')) {
|
||||
mainScreen.classList.remove('mobile-details');
|
||||
|
|
@ -1681,6 +1704,7 @@ function mobileNavBack() {
|
|||
setActiveSidebarIndicator(currentMainIndicator);
|
||||
}
|
||||
}
|
||||
|
||||
function mobileNavDetails() {
|
||||
mainScreen.classList.add('mobile-details');
|
||||
}
|
||||
|
|
@ -1711,6 +1735,10 @@ document.addEventListener('touchmove', e => {
|
|||
if (diffX > rem || diffY > rem) {
|
||||
touchMoved = true;
|
||||
|
||||
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
if (activeTouchButton) {
|
||||
activeTouchButton.classList.remove('is-pressed');
|
||||
activeTouchButton = null;
|
||||
|
|
@ -1757,6 +1785,7 @@ document.addEventListener('contextmenu', e => {
|
|||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
function handleMobileSwipe() {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
if (window.innerWidth > 52 * rem) return;
|
||||
|
|
@ -1842,6 +1871,19 @@ var loadedMessages = {};
|
|||
var oldestLoadedMsgId = null;
|
||||
var isLoadingOlderMessages = false;
|
||||
|
||||
|
||||
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
if (!unsafe) return "";
|
||||
return unsafe
|
||||
.replaceAll(/&/g, "&")
|
||||
.replaceAll(/</g, "<")
|
||||
.replaceAll(/>/g, ">")
|
||||
.replaceAll(/"/g, """)
|
||||
.replaceAll(/'/g, "'");
|
||||
}
|
||||
|
||||
async function openDm(dmId, username, targetId) {
|
||||
try {
|
||||
showAction("action.dm.opening", "dmopen");
|
||||
|
|
@ -1934,14 +1976,24 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
|
||||
for (let [msgId, msg] of entries) {
|
||||
let content = msg.content;
|
||||
|
||||
let reactions = msg.reactions || "";
|
||||
let decrypted = false;
|
||||
if (msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
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>`;
|
||||
decrypted = false;
|
||||
}
|
||||
} else {
|
||||
decrypted = true;
|
||||
}
|
||||
|
||||
if (decrypted) {
|
||||
content = escapeHtml(content);
|
||||
}
|
||||
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
|
||||
return processBlah(p1);
|
||||
|
|
@ -1967,6 +2019,9 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
dmPfpCache[msg.author] = await getAvatarUrl(msg.author, fullUsername);
|
||||
}
|
||||
}
|
||||
if (dmUsernameCache[msg.author] && !dmPfpCache[msg.author]) {
|
||||
dmPfpCache[msg.author] = await getAvatarUrl(msg.author, dmUsernameCache[msg.author]);
|
||||
}
|
||||
if (dmUsernameCache[msg.author]) {
|
||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||
pfp = dmPfpCache[msg.author];
|
||||
|
|
@ -1978,15 +2033,129 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
|
||||
let extraClass = showAvatar ? "with-avatar" : "";
|
||||
|
||||
let repliedHtml = "";
|
||||
if (msg.responded && msg.responded !== "") {
|
||||
let respondedId = msg.responded;
|
||||
if (typeof respondedId === 'number') respondedId = respondedId.toString();
|
||||
let respondedAuthorId = null;
|
||||
if (typeof respondedId === 'string' && respondedId.includes(':')) {
|
||||
let parts = respondedId.split(':');
|
||||
respondedId = parts[0];
|
||||
respondedAuthorId = parts[1];
|
||||
}
|
||||
|
||||
let replyAuthor = "Someone";
|
||||
let needsAsyncFetch = false;
|
||||
|
||||
if (loadedMessages[respondedId] && loadedMessages[respondedId].author !== "0") {
|
||||
let rAuthId = loadedMessages[respondedId].author;
|
||||
if (dmUsernameCache[rAuthId]) {
|
||||
replyAuthor = dmUsernameCache[rAuthId].split(':')[0];
|
||||
} else {
|
||||
needsAsyncFetch = true;
|
||||
respondedAuthorId = rAuthId;
|
||||
}
|
||||
} else if (respondedAuthorId && dmUsernameCache[respondedAuthorId]) {
|
||||
replyAuthor = dmUsernameCache[respondedAuthorId].split(':')[0];
|
||||
} else {
|
||||
needsAsyncFetch = true;
|
||||
}
|
||||
|
||||
if (window.replyAuthorCache && window.replyAuthorCache[respondedId]) {
|
||||
replyAuthor = window.replyAuthorCache[respondedId];
|
||||
needsAsyncFetch = false;
|
||||
}
|
||||
|
||||
let randId = "";
|
||||
if (needsAsyncFetch) {
|
||||
if (!window.replyAuthorCache) window.replyAuthorCache = {};
|
||||
window.replyIdCounter = (window.replyIdCounter || 0) + 1;
|
||||
if (window.replyIdCounter > 1000000) window.replyIdCounter = 0;
|
||||
randId = "reply-author-" + window.replyIdCounter;
|
||||
replyAuthor = "Someone";
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
let aId = respondedAuthorId;
|
||||
if (!aId) {
|
||||
let payload = JSON.stringify({
|
||||
string1: currentDmId,
|
||||
string2: "1",
|
||||
string3: respondedId
|
||||
});
|
||||
let res = await fetchEncrypted("dm/messages/get", payload);
|
||||
if (res && !res.startsWith("error:")) {
|
||||
let msgs = JSON.parse(res);
|
||||
if (msgs[respondedId] && msgs[respondedId].author !== "0") {
|
||||
aId = msgs[respondedId].author;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aId) {
|
||||
let fname = dmUsernameCache[aId];
|
||||
if (!fname) {
|
||||
fname = await fetchAsync(`${url}/idtoname?id=${aId}`);
|
||||
if (fname && !fname.startsWith("error:")) dmUsernameCache[aId] = fname;
|
||||
}
|
||||
if (fname && !fname.startsWith("error:")) {
|
||||
let sName = fname.split(':')[0];
|
||||
window.replyAuthorCache[respondedId] = sName;
|
||||
let el = document.getElementById(randId);
|
||||
if (el) {
|
||||
el.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${processBlah('title.replied.to')} ${sName}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
let divIdStr = randId !== "" ? `id="${randId}" ` : "";
|
||||
repliedHtml = `<div ${divIdStr}class="chat-message-replied" style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.2rem; cursor: pointer;" onclick="scrollToMessage('${respondedId}')"><svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${processBlah('title.replied.to')} ${replyAuthor}</div>`;
|
||||
}
|
||||
|
||||
let reactionsHtml = "";
|
||||
if (reactions) {
|
||||
try {
|
||||
let rxArr = JSON.parse(reactions);
|
||||
if (rxArr.length > 0) {
|
||||
let rxCounts = {};
|
||||
for (let rx of rxArr) {
|
||||
let parts = rx.split(':');
|
||||
if (parts.length < 2) continue;
|
||||
let rxVal = parts.slice(1).join(':');
|
||||
if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false};
|
||||
rxCounts[rxVal].count++;
|
||||
if (parts[0] === id) rxCounts[rxVal].me = true;
|
||||
}
|
||||
|
||||
let rxKeys = Object.keys(rxCounts);
|
||||
if (rxKeys.length > 0) {
|
||||
reactionsHtml = `<div class="chat-message-reactions" style="display: flex; gap: 0.3rem; flex-wrap: wrap; margin-top: 0.3rem;">`;
|
||||
for (let rxVal of rxKeys) {
|
||||
let style = rxCounts[rxVal].me ? "background: rgba(255, 255, 255, 0.12); border-color: var(--text-color);" : "background: rgba(255, 255, 255, 0.06);";
|
||||
let safeRx = encodeURIComponent(rxVal).replace(/'/g, "%27");
|
||||
let escapedRxVal = escapeHtml(rxVal);
|
||||
reactionsHtml += `<span class="reaction-pill" style="padding: 0.1rem 0.4rem; border-radius: 1rem; font-size: 0.85rem; border: var(--border-width) solid var(--light-border-color); cursor: pointer; ${style}" onclick="reactMessagePrompt('${msgId}', '${safeRx}')">${processBlah(escapedRxVal)} ${rxCounts[rxVal].count > 1 ? rxCounts[rxVal].count : ""}</span>`;
|
||||
}
|
||||
reactionsHtml += `</div>`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="chat-message ${extraClass}">
|
||||
<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">
|
||||
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')" ontouchstart="handleMessageTouchStart(event, '${msgId}')" ontouchend="handleMessageTouchEnd()" ontouchmove="handleMessageTouchMove()">
|
||||
${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>
|
||||
${reactionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -2013,24 +2182,52 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
}
|
||||
}
|
||||
|
||||
var lastChatScroll = 0;
|
||||
var isAdjusting = false;
|
||||
var ignoreScroll = false;
|
||||
|
||||
window.visualViewport?.addEventListener("resize", async () => {
|
||||
let container = document.getElementById("chat-messages");
|
||||
console.log(container.scrollHeight - container.clientHeight - lastChatScroll)
|
||||
|
||||
isAdjusting = true;
|
||||
while (isAdjusting) {
|
||||
ignoreScroll = true;
|
||||
container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll;
|
||||
requestAnimationFrame(() => {
|
||||
ignoreScroll = false;
|
||||
});
|
||||
await delay(10);
|
||||
}
|
||||
});
|
||||
function setupChatScrollListener() {
|
||||
let container = document.getElementById("chat-messages");
|
||||
if (!container) return;
|
||||
|
||||
container.onscroll = async () => {
|
||||
|
||||
if (!ignoreScroll) {
|
||||
isAdjusting = false;
|
||||
lastChatScroll = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
}
|
||||
|
||||
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
|
||||
isLoadingOlderMessages = true;
|
||||
showAction("info.messages.loading.older", "messages.loading.older");
|
||||
try {
|
||||
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
||||
isLoadingOlderMessages = false;
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {}
|
||||
clearAction("messages.loading.older");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
let replyingToMsgId = null;
|
||||
|
||||
async function sendMessage() {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
|
||||
|
|
@ -2038,6 +2235,21 @@ async function sendMessage() {
|
|||
let content = input.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
let pendingMsgId = "pending_" + Date.now();
|
||||
let container = document.getElementById("chat-messages");
|
||||
if (container) {
|
||||
let html = `
|
||||
<div class="chat-message" id="${pendingMsgId}" style="opacity: 0.5;">
|
||||
<div style="width: 2.5rem; flex-shrink: 0;"></div>
|
||||
<div class="chat-message-content">
|
||||
<div class="chat-message-text">${content} <span style="font-size: 0.8rem; opacity: 0.7;">(${processBlah("info.sending.message")})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||
|
|
@ -2045,23 +2257,33 @@ async function sendMessage() {
|
|||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: encryptedContent,
|
||||
string3: ""
|
||||
string3: "",
|
||||
string4: replyingToMsgId || ""
|
||||
};
|
||||
|
||||
input.value = "";
|
||||
input.style.height = '2.5rem';
|
||||
|
||||
window.forceScrollToBottom = true;
|
||||
replyingToMsgId = null;
|
||||
let bar = document.getElementById("replying-bar");
|
||||
if (bar) bar.style.display = "none";
|
||||
|
||||
showAction("action.message.sending", "msgsending");
|
||||
|
||||
let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
|
||||
clearAction("msgsending");
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
} else {
|
||||
await loadDmMessages(currentDmId);
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
clearAction("msgsending");
|
||||
console.error(e);
|
||||
showBlahNotification("error:message.send.failed");
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2118,3 +2340,212 @@ function setupWebSocket() {
|
|||
setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
let currentContextMenuMsgId = null;
|
||||
let touchTimeout = null;
|
||||
let touchTarget = null;
|
||||
|
||||
function handleMessageContextMenu(e, msgId) {
|
||||
e.preventDefault();
|
||||
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||
currentContextMenuMsgId = msgId;
|
||||
let elem = document.getElementById(`msg-${msgId}`);
|
||||
if (elem) elem.classList.add("context-menu-open");
|
||||
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
try {
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId
|
||||
};
|
||||
let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload));
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showBlahNotification("error:message.delete.failed");
|
||||
}
|
||||
clearAction("msgdel");
|
||||
}
|
||||
|
||||
function cancelReply() {
|
||||
replyingToMsgId = null;
|
||||
let bar = document.getElementById("replying-bar");
|
||||
if (bar) bar.style.display = "none";
|
||||
let input = document.getElementById("chat-input");
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
async function scrollToMessage(msgId) {
|
||||
let elem = document.getElementById(`msg-${msgId}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
elem.style.transition = "background-color 0.5s";
|
||||
let oldBg = elem.style.backgroundColor;
|
||||
elem.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
||||
setTimeout(() => {
|
||||
elem.style.backgroundColor = oldBg;
|
||||
}, 1500);
|
||||
} else {
|
||||
if (oldestLoadedMsgId !== null && parseInt(msgId) < oldestLoadedMsgId) {
|
||||
showAction("info.sending.message", "msgscroll");
|
||||
let tries = 0;
|
||||
while (!document.getElementById(`msg-${msgId}`) && oldestLoadedMsgId > 0 && tries < 15) {
|
||||
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
||||
tries++;
|
||||
}
|
||||
clearAction("msgscroll");
|
||||
let elem2 = document.getElementById(`msg-${msgId}`);
|
||||
if (elem2) {
|
||||
elem2.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
elem2.style.transition = "background-color 0.5s";
|
||||
let oldBg = elem2.style.backgroundColor;
|
||||
elem2.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
||||
setTimeout(() => {
|
||||
elem2.style.backgroundColor = oldBg;
|
||||
}, 1500);
|
||||
} else {
|
||||
showBlahNotification("error:message.not.found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function replyMessage(msgId) {
|
||||
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||
|
||||
let msg = loadedMessages[msgId];
|
||||
if (msg && msg.author) {
|
||||
replyingToMsgId = msgId + ":" + msg.author;
|
||||
} else {
|
||||
replyingToMsgId = msgId;
|
||||
}
|
||||
|
||||
let bar = document.getElementById("replying-bar");
|
||||
if (bar) {
|
||||
let msg = loadedMessages[msgId];
|
||||
let authorName = "Someone";
|
||||
if (msg && msg.author !== "0" && dmUsernameCache[msg.author]) {
|
||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||
}
|
||||
document.getElementById("replying-to-name").textContent = authorName;
|
||||
|
||||
let content = msg ? msg.content : "";
|
||||
if (msg && msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => processBlah(p1));
|
||||
content = processBlah(content);
|
||||
|
||||
document.getElementById("replying-to-text").textContent = content;
|
||||
bar.style.display = "flex";
|
||||
}
|
||||
let input = document.getElementById("chat-input");
|
||||
input.focus();
|
||||
}
|
||||
|
||||
async function reactMessagePrompt(msgId, quickReaction = null) {
|
||||
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||
let reaction = quickReaction;
|
||||
if (!reaction) {
|
||||
reaction = prompt("Enter reaction (emoji or text):");
|
||||
if (!reaction) return;
|
||||
} else {
|
||||
reaction = decodeURIComponent(reaction);
|
||||
}
|
||||
|
||||
showAction("action.message.reacting", "msgreact");
|
||||
try {
|
||||
let currentMsg = loadedMessages[msgId];
|
||||
let existingReactions = [];
|
||||
if (currentMsg && currentMsg.reactions) {
|
||||
let rxStr = currentMsg.reactions;
|
||||
if (currentMsg.key && currentMsg.key !== "") {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
try {
|
||||
rxStr = await decryptAesGcmFromBase64(rxStr, dmKeyBytes);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
existingReactions = JSON.parse(rxStr);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
let targetEntry = id + ":" + reaction;
|
||||
let index = existingReactions.indexOf(targetEntry);
|
||||
if (index > -1) {
|
||||
existingReactions.splice(index, 1);
|
||||
} else {
|
||||
existingReactions.push(targetEntry);
|
||||
}
|
||||
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
|
||||
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId,
|
||||
string3: encryptedReactions
|
||||
};
|
||||
let res = await fetchEncrypted("dm/message/react", JSON.stringify(msgPayload));
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showBlahNotification("error:message.react.failed");
|
||||
}
|
||||
clearAction("msgreact");
|
||||
}
|
||||
|
|
@ -133,6 +133,14 @@ 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 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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
||||
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
|
||||
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
||||
|
|
@ -220,6 +228,20 @@ var addSpaceMenu = `
|
|||
</button>
|
||||
`;
|
||||
|
||||
var messageContextMenu = `
|
||||
<button onclick="replyMessage(currentContextMenuMsgId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/></svg>
|
||||
<span class="blah">title.reply.message</span>
|
||||
</button>
|
||||
<button onclick="reactMessagePrompt(currentContextMenuMsgId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
<span class="blah">title.react.message</span>
|
||||
</button>
|
||||
<button id="context-delete-btn" onclick="deleteMessage(currentContextMenuMsgId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="1.4rem" viewBox="0 -960 960 960" fill="var(--big-red)"><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>
|
||||
<span class="blah" style="color: var(--big-red)">title.delete.message</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
//elements
|
||||
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
||||
|
|
|
|||
|
|
@ -630,7 +630,18 @@ space{
|
|||
.chat-message {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.2rem 0;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin: 0 -0.6rem;
|
||||
border-radius: 0.8rem;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.chat-message:hover {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
}
|
||||
.chat-message:active, .chat-message.context-menu-open {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.chat-message.with-avatar {
|
||||
margin-top: 0.8rem;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@
|
|||
"title.back.to.register": "back to registration",
|
||||
"placeholder.username": "cat name",
|
||||
"placeholder.captcha.code": "captcha code",
|
||||
"placeholder.message.input": "meow...",
|
||||
"desc.messages.loading": "loading meows...",
|
||||
"desc.no.dms": "no direct meowchats :c",
|
||||
"action.dm.opening": "opening meowchat...",
|
||||
|
|
@ -128,5 +127,18 @@
|
|||
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
|
||||
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
|
||||
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
||||
"messages.decrypt.failed": "failed to decrypt meow :c"
|
||||
"messages.decrypt.failed": "failed to decrypt meow :c",
|
||||
"title.reply.message": "meow back",
|
||||
"title.react.message": "purr",
|
||||
"title.delete.message": "hiss away",
|
||||
"title.replied.to": "meowed back to",
|
||||
"action.message.deleting": "hissing message away...",
|
||||
"action.message.sending": "sending meow...",
|
||||
"action.message.reacting": "adding purr...",
|
||||
"error:message.delete.failed": "failed to hiss away meow",
|
||||
"error:message.send.failed": "failed to send meow",
|
||||
"error:message.react.failed": "failed to add purr",
|
||||
"info.sending.message": "sending...",
|
||||
"placeholder.message.input": "meow...",
|
||||
"larp.redacted": "this meow was hissed away."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,5 +127,17 @@
|
|||
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
|
||||
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
|
||||
"dm.messages.fetch.failed": "Failed to fetch messages",
|
||||
"messages.decrypt.failed": "Failed to decrypt message"
|
||||
"messages.decrypt.failed": "Failed to decrypt message",
|
||||
"title.reply.message": "Reply",
|
||||
"title.react.message": "React",
|
||||
"title.delete.message": "Delete",
|
||||
"title.replied.to": "Replied to",
|
||||
"action.message.deleting": "Deleting message...",
|
||||
"action.message.sending": "Sending message...",
|
||||
"action.message.reacting": "Adding reaction...",
|
||||
"error:message.delete.failed": "Failed to delete message",
|
||||
"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."
|
||||
}
|
||||
|
|
|
|||
535
webroot/main.js
535
webroot/main.js
|
|
@ -1,18 +1,16 @@
|
|||
var prot = window.location.protocol;
|
||||
|
||||
async function updateProtocolAndUrl(host)
|
||||
{
|
||||
async function updateProtocolAndUrl(host) {
|
||||
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
||||
if (prot == "http:") {
|
||||
try {
|
||||
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
try {
|
||||
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
||||
prot = "https:";
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {}
|
||||
}
|
||||
}
|
||||
url = `${prot}//${host}/_larpix`;
|
||||
|
|
@ -744,6 +742,7 @@ function getInitials(name) {
|
|||
//1 word at least 2 letters
|
||||
return word.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function stringToColor(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
|
|
@ -755,6 +754,7 @@ function stringToColor(str) {
|
|||
|
||||
return `hsl(${h}, ${s}%, ${l}%)`;
|
||||
}
|
||||
|
||||
function createAvatarSvg(name, size = 512) {
|
||||
const initials = getInitials(name);
|
||||
const color = stringToColor(name);
|
||||
|
|
@ -777,17 +777,14 @@ function createAvatarSvg(name, size = 512) {
|
|||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
async function getAvatarUrl(id, username)
|
||||
{
|
||||
async function getAvatarUrl(id, username) {
|
||||
try {
|
||||
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
|
||||
if ((await fetchAsync(pfpUrl)) == "")
|
||||
{
|
||||
if ((await fetchAsync(pfpUrl)) == "") {
|
||||
throw Error();
|
||||
}
|
||||
return pfpUrl;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
return createAvatarSvg(username);
|
||||
}
|
||||
}
|
||||
|
|
@ -810,6 +807,7 @@ async function loadingFadeOut() {
|
|||
mainScreen.style.transform = "";
|
||||
mainScreen.style.opacity = "";
|
||||
}
|
||||
|
||||
async function loadingFadeIn() {
|
||||
|
||||
loadingScreen.style.transform = "scale(0.85)";
|
||||
|
|
@ -852,8 +850,7 @@ async function initBlahs() {
|
|||
|
||||
let placeholders = document.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -908,9 +905,7 @@ function processBlah(blahmessage) {
|
|||
message = message.replaceAll('{all}', valueslist);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
} catch (e) {
|
||||
if (prepended) {
|
||||
return blahmessage.substring(1);
|
||||
}
|
||||
|
|
@ -1082,6 +1077,7 @@ async function mainJS() {
|
|||
|
||||
await start();
|
||||
}
|
||||
|
||||
id = localStorage.getItem('id');
|
||||
username = localStorage.getItem('username');
|
||||
password = localStorage.getItem('password');
|
||||
|
|
@ -1104,8 +1100,7 @@ function showFixedContextMenu(rect, html) {
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1114,10 +1109,31 @@ function showFixedContextMenu(rect, html) {
|
|||
|
||||
fixedContextMenu.innerHTML = doc.body.innerHTML;
|
||||
|
||||
fixedContextMenu.style.left = `${rect.right + 10}px`;
|
||||
fixedContextMenu.style.top = `${rect.top}px`;
|
||||
fixedContextMenu.style.transition = 'none';
|
||||
fixedContextMenu.style.opacity = '0';
|
||||
fixedContextMenu.style.left = '0px';
|
||||
fixedContextMenu.style.top = '0px';
|
||||
|
||||
fixedContextMenu.classList.add("show");
|
||||
|
||||
let menuRect = fixedContextMenu.getBoundingClientRect();
|
||||
|
||||
let newLeft = rect.right + 10;
|
||||
let newTop = rect.top;
|
||||
|
||||
if (newLeft + menuRect.width > window.innerWidth) {
|
||||
newLeft = window.innerWidth - menuRect.width - 10;
|
||||
}
|
||||
if (newTop + menuRect.height > window.innerHeight) {
|
||||
newTop = window.innerHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
fixedContextMenu.style.left = `${newLeft}px`;
|
||||
fixedContextMenu.style.top = `${newTop}px`;
|
||||
|
||||
fixedContextMenu.offsetHeight;
|
||||
fixedContextMenu.style.transition = '';
|
||||
fixedContextMenu.style.opacity = '';
|
||||
}
|
||||
|
||||
var currentRoomsBarTitle = null;
|
||||
|
|
@ -1156,8 +1172,7 @@ async function switchRoomsBar(title, content) {
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1171,10 +1186,11 @@ async function switchRoomsBar(title, content) {
|
|||
roomsTopBarTransition.style.transform = "";
|
||||
roomsTopBarTransition.style.opacity = "";
|
||||
}
|
||||
|
||||
var currentRoomContentTitle = null;
|
||||
var currentRoomContentHtml = null;
|
||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false)
|
||||
{
|
||||
|
||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
||||
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
||||
|
|
@ -1203,9 +1219,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
|||
mainScreen.classList.remove('mobile-details');
|
||||
mainScreen.classList.add('mobile-content');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
await delay(200);
|
||||
}
|
||||
|
||||
|
|
@ -1229,8 +1243,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1246,10 +1259,11 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
|||
|
||||
|
||||
}
|
||||
|
||||
var currentDetailsContentTitle = null;
|
||||
var currentDetailsContentHtml = null;
|
||||
async function switchDetailsContent(title, content)
|
||||
{
|
||||
|
||||
async function switchDetailsContent(title, content) {
|
||||
if (currentDetailsContentTitle === title && currentDetailsContentHtml === content) return;
|
||||
currentDetailsContentTitle = title;
|
||||
currentDetailsContentHtml = content;
|
||||
|
|
@ -1284,8 +1298,7 @@ async function switchDetailsContent(title, content)
|
|||
|
||||
let placeholders = doc.querySelectorAll("[placeholder]");
|
||||
for (let i = 0; i < placeholders.length; i++) {
|
||||
if (placeholders[i].placeholder.startsWith("{blah("))
|
||||
{
|
||||
if (placeholders[i].placeholder.startsWith("{blah(")) {
|
||||
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
|
||||
placeholders[i].placeholder = processBlah(value);
|
||||
}
|
||||
|
|
@ -1301,8 +1314,8 @@ async function switchDetailsContent(title, content)
|
|||
|
||||
|
||||
}
|
||||
function clickCollapseDms()
|
||||
{
|
||||
|
||||
function clickCollapseDms() {
|
||||
var collapseDmsBtn = document.getElementById("collapse-dms");
|
||||
collapseDmsBtn.classList.toggle("collapsed");
|
||||
var dmsWrapper = document.getElementById("dms-wrapper");
|
||||
|
|
@ -1314,8 +1327,8 @@ function clickCollapseDms()
|
|||
}
|
||||
}
|
||||
}
|
||||
function clickCollapseGroups()
|
||||
{
|
||||
|
||||
function clickCollapseGroups() {
|
||||
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
||||
collapseGroupsBtn.classList.toggle("collapsed");
|
||||
var groupsWrapper = document.getElementById("groups-wrapper");
|
||||
|
|
@ -1327,12 +1340,12 @@ function clickCollapseGroups()
|
|||
}
|
||||
}
|
||||
}
|
||||
function clickAddGroup()
|
||||
{
|
||||
|
||||
function clickAddGroup() {
|
||||
switchRoomContent("title.create.group", createGroupScreen, false);
|
||||
}
|
||||
function clickAddDm()
|
||||
{
|
||||
|
||||
function clickAddDm() {
|
||||
switchRoomContent("title.create.dm", addDmScreen, false);
|
||||
}
|
||||
|
||||
|
|
@ -1353,12 +1366,20 @@ sidebarAddButton.addEventListener("click", (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
function clearContextMenuStyles() {
|
||||
let elems = document.querySelectorAll(".chat-message.context-menu-open");
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
elems[i].classList.remove("context-menu-open");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
history.pushState({trap: true}, "", location.href);
|
||||
|
||||
if (fixedContextMenu.classList.contains("show")) {
|
||||
if (!fixedContextMenu.contains(e.target)) {
|
||||
fixedContextMenu.classList.remove("show");
|
||||
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||
if (!e.target.closest('sidebarelement')) {
|
||||
setActiveSidebarIndicator(currentMainIndicator);
|
||||
}
|
||||
|
|
@ -1384,10 +1405,9 @@ if (App) {
|
|||
popstate();
|
||||
});
|
||||
}
|
||||
function popstate()
|
||||
{
|
||||
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details'))
|
||||
{
|
||||
|
||||
function popstate() {
|
||||
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
|
||||
history.back();
|
||||
if (App) {
|
||||
App.minimizeApp();
|
||||
|
|
@ -1400,6 +1420,7 @@ function popstate()
|
|||
function gotoSideProfilePopup() {
|
||||
|
||||
}
|
||||
|
||||
function setActiveRoombarItem(itemId) {
|
||||
let items = document.querySelectorAll('.collapse-text-button');
|
||||
items.forEach(item => item.classList.remove('selected'));
|
||||
|
|
@ -1529,6 +1550,7 @@ async function switchInvitesTab(tab) {
|
|||
|
||||
await loadInvites(tab);
|
||||
}
|
||||
|
||||
async function acceptInvite(targetId) { //TODO: Implement key generation
|
||||
try {
|
||||
showAction("action.invite.accepting", "invite.action");
|
||||
|
|
@ -1663,6 +1685,7 @@ function setActiveSidebarIndicator(element, isTemporary = false) {
|
|||
currentMainIndicator = element;
|
||||
}
|
||||
}
|
||||
|
||||
function mobileNavBack() {
|
||||
if (mainScreen.classList.contains('mobile-details')) {
|
||||
mainScreen.classList.remove('mobile-details');
|
||||
|
|
@ -1681,6 +1704,7 @@ function mobileNavBack() {
|
|||
setActiveSidebarIndicator(currentMainIndicator);
|
||||
}
|
||||
}
|
||||
|
||||
function mobileNavDetails() {
|
||||
mainScreen.classList.add('mobile-details');
|
||||
}
|
||||
|
|
@ -1711,6 +1735,10 @@ document.addEventListener('touchmove', e => {
|
|||
if (diffX > rem || diffY > rem) {
|
||||
touchMoved = true;
|
||||
|
||||
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
if (activeTouchButton) {
|
||||
activeTouchButton.classList.remove('is-pressed');
|
||||
activeTouchButton = null;
|
||||
|
|
@ -1757,6 +1785,7 @@ document.addEventListener('contextmenu', e => {
|
|||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
function handleMobileSwipe() {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
if (window.innerWidth > 52 * rem) return;
|
||||
|
|
@ -1842,6 +1871,19 @@ var loadedMessages = {};
|
|||
var oldestLoadedMsgId = null;
|
||||
var isLoadingOlderMessages = false;
|
||||
|
||||
|
||||
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
if (!unsafe) return "";
|
||||
return unsafe
|
||||
.replaceAll(/&/g, "&")
|
||||
.replaceAll(/</g, "<")
|
||||
.replaceAll(/>/g, ">")
|
||||
.replaceAll(/"/g, """)
|
||||
.replaceAll(/'/g, "'");
|
||||
}
|
||||
|
||||
async function openDm(dmId, username, targetId) {
|
||||
try {
|
||||
showAction("action.dm.opening", "dmopen");
|
||||
|
|
@ -1934,14 +1976,24 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
|
||||
for (let [msgId, msg] of entries) {
|
||||
let content = msg.content;
|
||||
|
||||
let reactions = msg.reactions || "";
|
||||
let decrypted = false;
|
||||
if (msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
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>`;
|
||||
decrypted = false;
|
||||
}
|
||||
} else {
|
||||
decrypted = true;
|
||||
}
|
||||
|
||||
if (decrypted) {
|
||||
content = escapeHtml(content);
|
||||
}
|
||||
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => {
|
||||
return processBlah(p1);
|
||||
|
|
@ -1967,6 +2019,9 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
dmPfpCache[msg.author] = await getAvatarUrl(msg.author, fullUsername);
|
||||
}
|
||||
}
|
||||
if (dmUsernameCache[msg.author] && !dmPfpCache[msg.author]) {
|
||||
dmPfpCache[msg.author] = await getAvatarUrl(msg.author, dmUsernameCache[msg.author]);
|
||||
}
|
||||
if (dmUsernameCache[msg.author]) {
|
||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||
pfp = dmPfpCache[msg.author];
|
||||
|
|
@ -1978,15 +2033,129 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
|
||||
let extraClass = showAvatar ? "with-avatar" : "";
|
||||
|
||||
let repliedHtml = "";
|
||||
if (msg.responded && msg.responded !== "") {
|
||||
let respondedId = msg.responded;
|
||||
if (typeof respondedId === 'number') respondedId = respondedId.toString();
|
||||
let respondedAuthorId = null;
|
||||
if (typeof respondedId === 'string' && respondedId.includes(':')) {
|
||||
let parts = respondedId.split(':');
|
||||
respondedId = parts[0];
|
||||
respondedAuthorId = parts[1];
|
||||
}
|
||||
|
||||
let replyAuthor = "Someone";
|
||||
let needsAsyncFetch = false;
|
||||
|
||||
if (loadedMessages[respondedId] && loadedMessages[respondedId].author !== "0") {
|
||||
let rAuthId = loadedMessages[respondedId].author;
|
||||
if (dmUsernameCache[rAuthId]) {
|
||||
replyAuthor = dmUsernameCache[rAuthId].split(':')[0];
|
||||
} else {
|
||||
needsAsyncFetch = true;
|
||||
respondedAuthorId = rAuthId;
|
||||
}
|
||||
} else if (respondedAuthorId && dmUsernameCache[respondedAuthorId]) {
|
||||
replyAuthor = dmUsernameCache[respondedAuthorId].split(':')[0];
|
||||
} else {
|
||||
needsAsyncFetch = true;
|
||||
}
|
||||
|
||||
if (window.replyAuthorCache && window.replyAuthorCache[respondedId]) {
|
||||
replyAuthor = window.replyAuthorCache[respondedId];
|
||||
needsAsyncFetch = false;
|
||||
}
|
||||
|
||||
let randId = "";
|
||||
if (needsAsyncFetch) {
|
||||
if (!window.replyAuthorCache) window.replyAuthorCache = {};
|
||||
window.replyIdCounter = (window.replyIdCounter || 0) + 1;
|
||||
if (window.replyIdCounter > 1000000) window.replyIdCounter = 0;
|
||||
randId = "reply-author-" + window.replyIdCounter;
|
||||
replyAuthor = "Someone";
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
let aId = respondedAuthorId;
|
||||
if (!aId) {
|
||||
let payload = JSON.stringify({
|
||||
string1: currentDmId,
|
||||
string2: "1",
|
||||
string3: respondedId
|
||||
});
|
||||
let res = await fetchEncrypted("dm/messages/get", payload);
|
||||
if (res && !res.startsWith("error:")) {
|
||||
let msgs = JSON.parse(res);
|
||||
if (msgs[respondedId] && msgs[respondedId].author !== "0") {
|
||||
aId = msgs[respondedId].author;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aId) {
|
||||
let fname = dmUsernameCache[aId];
|
||||
if (!fname) {
|
||||
fname = await fetchAsync(`${url}/idtoname?id=${aId}`);
|
||||
if (fname && !fname.startsWith("error:")) dmUsernameCache[aId] = fname;
|
||||
}
|
||||
if (fname && !fname.startsWith("error:")) {
|
||||
let sName = fname.split(':')[0];
|
||||
window.replyAuthorCache[respondedId] = sName;
|
||||
let el = document.getElementById(randId);
|
||||
if (el) {
|
||||
el.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${processBlah('title.replied.to')} ${sName}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
let divIdStr = randId !== "" ? `id="${randId}" ` : "";
|
||||
repliedHtml = `<div ${divIdStr}class="chat-message-replied" style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.2rem; cursor: pointer;" onclick="scrollToMessage('${respondedId}')"><svg xmlns="http://www.w3.org/2000/svg" width="0.8rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg> ${processBlah('title.replied.to')} ${replyAuthor}</div>`;
|
||||
}
|
||||
|
||||
let reactionsHtml = "";
|
||||
if (reactions) {
|
||||
try {
|
||||
let rxArr = JSON.parse(reactions);
|
||||
if (rxArr.length > 0) {
|
||||
let rxCounts = {};
|
||||
for (let rx of rxArr) {
|
||||
let parts = rx.split(':');
|
||||
if (parts.length < 2) continue;
|
||||
let rxVal = parts.slice(1).join(':');
|
||||
if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false};
|
||||
rxCounts[rxVal].count++;
|
||||
if (parts[0] === id) rxCounts[rxVal].me = true;
|
||||
}
|
||||
|
||||
let rxKeys = Object.keys(rxCounts);
|
||||
if (rxKeys.length > 0) {
|
||||
reactionsHtml = `<div class="chat-message-reactions" style="display: flex; gap: 0.3rem; flex-wrap: wrap; margin-top: 0.3rem;">`;
|
||||
for (let rxVal of rxKeys) {
|
||||
let style = rxCounts[rxVal].me ? "background: rgba(255, 255, 255, 0.12); border-color: var(--text-color);" : "background: rgba(255, 255, 255, 0.06);";
|
||||
let safeRx = encodeURIComponent(rxVal).replace(/'/g, "%27");
|
||||
let escapedRxVal = escapeHtml(rxVal);
|
||||
reactionsHtml += `<span class="reaction-pill" style="padding: 0.1rem 0.4rem; border-radius: 1rem; font-size: 0.85rem; border: var(--border-width) solid var(--light-border-color); cursor: pointer; ${style}" onclick="reactMessagePrompt('${msgId}', '${safeRx}')">${processBlah(escapedRxVal)} ${rxCounts[rxVal].count > 1 ? rxCounts[rxVal].count : ""}</span>`;
|
||||
}
|
||||
reactionsHtml += `</div>`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="chat-message ${extraClass}">
|
||||
<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">
|
||||
<div class="chat-message-content" oncontextmenu="handleMessageContextMenu(event, '${msgId}')" ontouchstart="handleMessageTouchStart(event, '${msgId}')" ontouchend="handleMessageTouchEnd()" ontouchmove="handleMessageTouchMove()">
|
||||
${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>
|
||||
${reactionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -2013,24 +2182,52 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
}
|
||||
}
|
||||
|
||||
var lastChatScroll = 0;
|
||||
var isAdjusting = false;
|
||||
var ignoreScroll = false;
|
||||
|
||||
window.visualViewport?.addEventListener("resize", async () => {
|
||||
let container = document.getElementById("chat-messages");
|
||||
console.log(container.scrollHeight - container.clientHeight - lastChatScroll)
|
||||
|
||||
isAdjusting = true;
|
||||
while (isAdjusting) {
|
||||
ignoreScroll = true;
|
||||
container.scrollTop = container.scrollHeight - container.clientHeight - lastChatScroll;
|
||||
requestAnimationFrame(() => {
|
||||
ignoreScroll = false;
|
||||
});
|
||||
await delay(10);
|
||||
}
|
||||
});
|
||||
function setupChatScrollListener() {
|
||||
let container = document.getElementById("chat-messages");
|
||||
if (!container) return;
|
||||
|
||||
container.onscroll = async () => {
|
||||
|
||||
if (!ignoreScroll) {
|
||||
isAdjusting = false;
|
||||
lastChatScroll = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
}
|
||||
|
||||
if (container.scrollTop === 0 && !isLoadingOlderMessages && oldestLoadedMsgId !== null && oldestLoadedMsgId > 0) {
|
||||
isLoadingOlderMessages = true;
|
||||
showAction("info.messages.loading.older", "messages.loading.older");
|
||||
try {
|
||||
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
||||
isLoadingOlderMessages = false;
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {}
|
||||
clearAction("messages.loading.older");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
let replyingToMsgId = null;
|
||||
|
||||
async function sendMessage() {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
|
||||
|
|
@ -2038,6 +2235,21 @@ async function sendMessage() {
|
|||
let content = input.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
let pendingMsgId = "pending_" + Date.now();
|
||||
let container = document.getElementById("chat-messages");
|
||||
if (container) {
|
||||
let html = `
|
||||
<div class="chat-message" id="${pendingMsgId}" style="opacity: 0.5;">
|
||||
<div style="width: 2.5rem; flex-shrink: 0;"></div>
|
||||
<div class="chat-message-content">
|
||||
<div class="chat-message-text">${content} <span style="font-size: 0.8rem; opacity: 0.7;">(${processBlah("info.sending.message")})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||
|
|
@ -2045,23 +2257,33 @@ async function sendMessage() {
|
|||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: encryptedContent,
|
||||
string3: ""
|
||||
string3: "",
|
||||
string4: replyingToMsgId || ""
|
||||
};
|
||||
|
||||
input.value = "";
|
||||
input.style.height = '2.5rem';
|
||||
|
||||
window.forceScrollToBottom = true;
|
||||
replyingToMsgId = null;
|
||||
let bar = document.getElementById("replying-bar");
|
||||
if (bar) bar.style.display = "none";
|
||||
|
||||
showAction("action.message.sending", "msgsending");
|
||||
|
||||
let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload));
|
||||
clearAction("msgsending");
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
} else {
|
||||
await loadDmMessages(currentDmId);
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
clearAction("msgsending");
|
||||
console.error(e);
|
||||
showBlahNotification("error:message.send.failed");
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2118,3 +2340,212 @@ function setupWebSocket() {
|
|||
setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
let currentContextMenuMsgId = null;
|
||||
let touchTimeout = null;
|
||||
let touchTarget = null;
|
||||
|
||||
function handleMessageContextMenu(e, msgId) {
|
||||
e.preventDefault();
|
||||
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
|
||||
currentContextMenuMsgId = msgId;
|
||||
let elem = document.getElementById(`msg-${msgId}`);
|
||||
if (elem) elem.classList.add("context-menu-open");
|
||||
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
try {
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId
|
||||
};
|
||||
let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload));
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showBlahNotification("error:message.delete.failed");
|
||||
}
|
||||
clearAction("msgdel");
|
||||
}
|
||||
|
||||
function cancelReply() {
|
||||
replyingToMsgId = null;
|
||||
let bar = document.getElementById("replying-bar");
|
||||
if (bar) bar.style.display = "none";
|
||||
let input = document.getElementById("chat-input");
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
async function scrollToMessage(msgId) {
|
||||
let elem = document.getElementById(`msg-${msgId}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
elem.style.transition = "background-color 0.5s";
|
||||
let oldBg = elem.style.backgroundColor;
|
||||
elem.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
||||
setTimeout(() => {
|
||||
elem.style.backgroundColor = oldBg;
|
||||
}, 1500);
|
||||
} else {
|
||||
if (oldestLoadedMsgId !== null && parseInt(msgId) < oldestLoadedMsgId) {
|
||||
showAction("info.sending.message", "msgscroll");
|
||||
let tries = 0;
|
||||
while (!document.getElementById(`msg-${msgId}`) && oldestLoadedMsgId > 0 && tries < 15) {
|
||||
await loadDmMessages(currentDmId, (oldestLoadedMsgId - 1).toString(), true);
|
||||
tries++;
|
||||
}
|
||||
clearAction("msgscroll");
|
||||
let elem2 = document.getElementById(`msg-${msgId}`);
|
||||
if (elem2) {
|
||||
elem2.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
elem2.style.transition = "background-color 0.5s";
|
||||
let oldBg = elem2.style.backgroundColor;
|
||||
elem2.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
||||
setTimeout(() => {
|
||||
elem2.style.backgroundColor = oldBg;
|
||||
}, 1500);
|
||||
} else {
|
||||
showBlahNotification("error:message.not.found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function replyMessage(msgId) {
|
||||
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||
|
||||
let msg = loadedMessages[msgId];
|
||||
if (msg && msg.author) {
|
||||
replyingToMsgId = msgId + ":" + msg.author;
|
||||
} else {
|
||||
replyingToMsgId = msgId;
|
||||
}
|
||||
|
||||
let bar = document.getElementById("replying-bar");
|
||||
if (bar) {
|
||||
let msg = loadedMessages[msgId];
|
||||
let authorName = "Someone";
|
||||
if (msg && msg.author !== "0" && dmUsernameCache[msg.author]) {
|
||||
authorName = dmUsernameCache[msg.author].split(':')[0];
|
||||
}
|
||||
document.getElementById("replying-to-name").textContent = authorName;
|
||||
|
||||
let content = msg ? msg.content : "";
|
||||
if (msg && msg.key && msg.key !== "") {
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
content = await decryptAesGcmFromBase64(content, dmKeyBytes);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
content = content.replace(/\{blah\((.*?)\)\}/g, (match, p1) => processBlah(p1));
|
||||
content = processBlah(content);
|
||||
|
||||
document.getElementById("replying-to-text").textContent = content;
|
||||
bar.style.display = "flex";
|
||||
}
|
||||
let input = document.getElementById("chat-input");
|
||||
input.focus();
|
||||
}
|
||||
|
||||
async function reactMessagePrompt(msgId, quickReaction = null) {
|
||||
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
|
||||
let reaction = quickReaction;
|
||||
if (!reaction) {
|
||||
reaction = prompt("Enter reaction (emoji or text):");
|
||||
if (!reaction) return;
|
||||
} else {
|
||||
reaction = decodeURIComponent(reaction);
|
||||
}
|
||||
|
||||
showAction("action.message.reacting", "msgreact");
|
||||
try {
|
||||
let currentMsg = loadedMessages[msgId];
|
||||
let existingReactions = [];
|
||||
if (currentMsg && currentMsg.reactions) {
|
||||
let rxStr = currentMsg.reactions;
|
||||
if (currentMsg.key && currentMsg.key !== "") {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
try {
|
||||
rxStr = await decryptAesGcmFromBase64(rxStr, dmKeyBytes);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
existingReactions = JSON.parse(rxStr);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
let targetEntry = id + ":" + reaction;
|
||||
let index = existingReactions.indexOf(targetEntry);
|
||||
if (index > -1) {
|
||||
existingReactions.splice(index, 1);
|
||||
} else {
|
||||
existingReactions.push(targetEntry);
|
||||
}
|
||||
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedReactions = await encryptAesGcmToBase64(JSON.stringify(existingReactions), dmKeyBytes);
|
||||
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: msgId,
|
||||
string3: encryptedReactions
|
||||
};
|
||||
let res = await fetchEncrypted("dm/message/react", JSON.stringify(msgPayload));
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showBlahNotification("error:message.react.failed");
|
||||
}
|
||||
clearAction("msgreact");
|
||||
}
|
||||
|
|
@ -133,6 +133,14 @@ 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 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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
||||
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
|
||||
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
||||
|
|
@ -220,6 +228,20 @@ var addSpaceMenu = `
|
|||
</button>
|
||||
`;
|
||||
|
||||
var messageContextMenu = `
|
||||
<button onclick="replyMessage(currentContextMenuMsgId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/></svg>
|
||||
<span class="blah">title.reply.message</span>
|
||||
</button>
|
||||
<button onclick="reactMessagePrompt(currentContextMenuMsgId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
<span class="blah">title.react.message</span>
|
||||
</button>
|
||||
<button id="context-delete-btn" onclick="deleteMessage(currentContextMenuMsgId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="1.4rem" viewBox="0 -960 960 960" fill="var(--big-red)"><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>
|
||||
<span class="blah" style="color: var(--big-red)">title.delete.message</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
//elements
|
||||
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
||||
|
|
|
|||
|
|
@ -630,7 +630,18 @@ space{
|
|||
.chat-message {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.2rem 0;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin: 0 -0.6rem;
|
||||
border-radius: 0.8rem;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.chat-message:hover {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
}
|
||||
.chat-message:active, .chat-message.context-menu-open {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.chat-message.with-avatar {
|
||||
margin-top: 0.8rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue