add attachments
This commit is contained in:
parent
0bdfb7762f
commit
1800b89a6b
8 changed files with 804 additions and 28 deletions
|
|
@ -184,5 +184,12 @@
|
|||
"success:banner.updated": "Cat banner updated successfully :3",
|
||||
"action.save": "Save :3",
|
||||
"action.delete.account": "Delete cat",
|
||||
"account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3"
|
||||
"account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3",
|
||||
"action.attachment.take.photo": "Meow photo",
|
||||
"action.attachment.media": "Meowdia",
|
||||
"action.attachment.file": "Meowle",
|
||||
"action.attachment.poll": "Meowll",
|
||||
"action.attachment.uploading": "Uploading meowttachments...",
|
||||
"attachment.legacy": "Legacy meowttachment",
|
||||
"warning.unencrypted": "This meowsage is not encrypted"
|
||||
}
|
||||
|
|
@ -184,5 +184,12 @@
|
|||
"banner.updated": "Banner updated successfully",
|
||||
"action.save": "Save",
|
||||
"action.delete.account": "Delete account",
|
||||
"account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process."
|
||||
"account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process.",
|
||||
"action.attachment.take.photo": "Take photo",
|
||||
"action.attachment.media": "Media",
|
||||
"action.attachment.file": "File",
|
||||
"action.attachment.poll": "Poll",
|
||||
"action.attachment.uploading": "Uploading attachments...",
|
||||
"attachment.legacy": "Legacy attachment",
|
||||
"warning.unencrypted": "This message is not encrypted"
|
||||
}
|
||||
365
webroot/main.js
365
webroot/main.js
|
|
@ -243,6 +243,24 @@ async function decryptAesGcmFromBase64(cipherBase64, keyBytes) {
|
|||
return new TextDecoder().decode(ptBuf);
|
||||
}
|
||||
|
||||
async function encryptAesGcmToBytes(plainBytes, keyBytes) {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const key = await importAesGcmKey(keyBytes);
|
||||
const ctBuf = await crypto.subtle.encrypt({name: "AES-GCM", iv}, key, plainBytes);
|
||||
const combined = new Uint8Array(iv.length + ctBuf.byteLength);
|
||||
combined.set(iv, 0);
|
||||
combined.set(new Uint8Array(ctBuf), iv.length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
async function decryptAesGcmFromBytes(cipherBytes, keyBytes) {
|
||||
const iv = cipherBytes.slice(0, 12);
|
||||
const ct = cipherBytes.slice(12);
|
||||
const key = await importAesGcmKey(keyBytes);
|
||||
const ptBuf = await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ct);
|
||||
return new Uint8Array(ptBuf);
|
||||
}
|
||||
|
||||
function randomRoomKey256Chars() {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(192));
|
||||
return uint8ToBase64(bytes);
|
||||
|
|
@ -1424,6 +1442,24 @@ function clickAddDm() {
|
|||
switchRoomContent("title.create.dm", addDmScreen, false);
|
||||
}
|
||||
|
||||
function clickAddAttachment(btn, e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
history.pushState({trap: true}, "", location.href);
|
||||
|
||||
if (fixedContextMenu.classList.contains("show")) {
|
||||
fixedContextMenu.classList.remove("show");
|
||||
} else {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
showFixedContextMenu(rect, attachmentMenu);
|
||||
|
||||
// Let's reposition it so it appears properly anchored above the input if it's too close to the bottom.
|
||||
// showFixedContextMenu automatically handles bounds, but it's set to top-right alignment basically.
|
||||
// It's perfectly fine to rely on the default bounds checking of showFixedContextMenu.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var currentMainIndicator = null;
|
||||
sidebarAddButton.addEventListener("click", (e) => {
|
||||
|
|
@ -2285,6 +2321,31 @@ async function loadDmMessages(dmId, startOffset = "", isPrepend = false) {
|
|||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (!+bytes) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
function truncateFilename(name, maxLength = 20) {
|
||||
if (name.length <= maxLength) return name;
|
||||
let extIndex = name.lastIndexOf(".");
|
||||
if (extIndex === -1 || extIndex === 0) {
|
||||
return name.substring(0, Math.floor(maxLength / 2) - 1) + "..." + name.substring(name.length - Math.floor(maxLength / 2) + 2);
|
||||
}
|
||||
let ext = name.substring(extIndex);
|
||||
let baseName = name.substring(0, extIndex);
|
||||
let allowedBaseLength = maxLength - ext.length - 3; // 3 for "..."
|
||||
if (allowedBaseLength <= 1) {
|
||||
return name.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
let half = Math.floor(allowedBaseLength / 2);
|
||||
return baseName.substring(0, half) + "..." + baseName.substring(baseName.length - (allowedBaseLength - half)) + ext;
|
||||
}
|
||||
|
||||
async function renderMessages(messages, isPrepend = false) {
|
||||
let container = document.getElementById("chat-messages");
|
||||
if (!container) return;
|
||||
|
|
@ -2312,6 +2373,46 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
decrypted = true;
|
||||
}
|
||||
|
||||
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>` : ``;
|
||||
|
||||
let attachmentsHtml = "";
|
||||
if (msg.attachment && msg.attachment !== "") {
|
||||
if (msg.attachment.startsWith("[")) {
|
||||
try {
|
||||
let atts = JSON.parse(msg.attachment);
|
||||
if (atts.length > 0) {
|
||||
attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">`;
|
||||
for (let att of atts) {
|
||||
let attContainerId = `att-container-${msgId}-${att.id}`;
|
||||
let isMedia = att.type.startsWith("image/") || att.type.startsWith("video/");
|
||||
let sizeStr = att.size ? formatBytes(att.size) : "";
|
||||
let displayName = truncateFilename(att.name, 25);
|
||||
let iconSvg = isMedia
|
||||
? `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`
|
||||
: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||
|
||||
attachmentsHtml += `<div id="${attContainerId}" class="attachment-box" style="position: relative; min-width: 120px; max-width: 300px; min-height: 100px; max-height: 300px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;" onclick="downloadAndRenderAttachment('${msgId}', '${att.id}', '${escapeHtml(att.type)}', '${escapeHtml(att.name)}')">
|
||||
<div style="padding: 1rem; text-align: center; color: var(--text-color);">
|
||||
${iconSvg}
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; word-break: break-all;">${escapeHtml(displayName)}</div>
|
||||
${sizeStr ? `<div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.2rem;">${escapeHtml(sizeStr)}</div>` : ""}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
attachmentsHtml += `</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse attachments", e);
|
||||
}
|
||||
} else {
|
||||
attachmentsHtml += `<div class="chat-message-attachments" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
|
||||
<div style="padding: 0.5rem; background: rgba(0,0,0,0.2); border: var(--border-width) solid var(--border-color); border-radius: 0.5rem; font-size: 0.8rem; opacity: 0.7;">[${processBlah("attachment.legacy")}: ${escapeHtml(msg.attachment)}]</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (decrypted) {
|
||||
content = escapeHtml(content);
|
||||
}
|
||||
|
|
@ -2352,6 +2453,8 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
let timeStr = date.toLocaleString();
|
||||
|
||||
let extraClass = showAvatar ? "with-avatar" : "";
|
||||
|
||||
let unencryptedIcon = (!decrypted && !isRedacted) ? `<span title="${processBlah('warning.unencrypted')}" style="color: var(--big-red); vertical-align: -0.165em; margin-right: 0.2rem; cursor: help;"><svg xmlns="http://www.w3.org/2000/svg" width="1rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg></span>` : ``;
|
||||
|
||||
let repliedHtml = "";
|
||||
if (msg.responded && msg.responded !== "") {
|
||||
|
|
@ -2465,10 +2568,6 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
}
|
||||
}
|
||||
|
||||
let isRedacted = msg.type === "larp.redacted";
|
||||
let redactedStyle = isRedacted ? ` opacity: 0.5; font-style: italic;` : ``;
|
||||
let redactedIcon = isRedacted ? `<svg xmlns="http://www.w3.org/2000/svg" height="1rem" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: -0.165em; margin-right: 0.2rem;"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>` : ``;
|
||||
|
||||
html += `
|
||||
<div class="chat-message ${extraClass}" data-msg-id="${msgId}" id="msg-${msgId}">
|
||||
${showAvatar ? `<img src="${pfp}" class="chat-message-pfp" onclick="openProfile('${msg.author}')" style="cursor: pointer;">` : `<div style="width: 2.5rem; flex-shrink: 0;"></div>`}
|
||||
|
|
@ -2478,7 +2577,8 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
<span class="chat-message-timestamp">${timeStr}</span>
|
||||
</div>` : ""}
|
||||
${repliedHtml}
|
||||
<div class="chat-message-text" style="${redactedStyle}">${redactedIcon}${content}</div>
|
||||
<div class="chat-message-text" style="${redactedStyle}">${unencryptedIcon}${redactedIcon}${content}</div>
|
||||
${attachmentsHtml}
|
||||
${reactionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2495,7 +2595,76 @@ async function renderMessages(messages, isPrepend = false) {
|
|||
let oldScrollTop = container.scrollTop;
|
||||
let oldScrollHeight = container.scrollHeight;
|
||||
|
||||
container.innerHTML = html;
|
||||
let tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
let newChildren = Array.from(tempDiv.children);
|
||||
|
||||
let currentIndex = 0;
|
||||
for (let newChild of newChildren) {
|
||||
let existingChild = document.getElementById(newChild.id);
|
||||
|
||||
if (existingChild && existingChild.parentElement === container) {
|
||||
if (container.children[currentIndex] !== existingChild) {
|
||||
container.insertBefore(existingChild, container.children[currentIndex]);
|
||||
}
|
||||
|
||||
let oldText = existingChild.querySelector('.chat-message-text');
|
||||
let newText = newChild.querySelector('.chat-message-text');
|
||||
if (oldText && newText && oldText.innerHTML !== newText.innerHTML) {
|
||||
oldText.innerHTML = newText.innerHTML;
|
||||
}
|
||||
|
||||
let oldReactions = existingChild.querySelector('.chat-message-reactions');
|
||||
let newReactions = newChild.querySelector('.chat-message-reactions');
|
||||
if (newReactions) {
|
||||
if (oldReactions) {
|
||||
if (oldReactions.innerHTML !== newReactions.innerHTML) oldReactions.innerHTML = newReactions.innerHTML;
|
||||
} else {
|
||||
existingChild.querySelector('.chat-message-content').appendChild(newReactions);
|
||||
}
|
||||
} else if (oldReactions) {
|
||||
oldReactions.remove();
|
||||
}
|
||||
|
||||
let oldPfp = existingChild.querySelector('.chat-message-pfp');
|
||||
let newPfp = newChild.querySelector('.chat-message-pfp');
|
||||
if (oldPfp && newPfp && oldPfp.src !== newPfp.src) {
|
||||
oldPfp.src = newPfp.src;
|
||||
}
|
||||
|
||||
let oldHeader = existingChild.querySelector('.chat-message-header');
|
||||
let newHeader = newChild.querySelector('.chat-message-header');
|
||||
if (oldHeader && newHeader && oldHeader.innerHTML !== newHeader.innerHTML) {
|
||||
oldHeader.innerHTML = newHeader.innerHTML;
|
||||
}
|
||||
|
||||
let oldReplied = existingChild.querySelector('.chat-message-replied');
|
||||
let newReplied = newChild.querySelector('.chat-message-replied');
|
||||
if (newReplied) {
|
||||
if (oldReplied) {
|
||||
if (oldReplied.innerHTML !== newReplied.innerHTML) oldReplied.innerHTML = newReplied.innerHTML;
|
||||
} else {
|
||||
let contentNode = existingChild.querySelector('.chat-message-content');
|
||||
if (oldText) contentNode.insertBefore(newReplied, oldText);
|
||||
else contentNode.insertBefore(newReplied, contentNode.firstChild);
|
||||
}
|
||||
} else if (oldReplied) {
|
||||
oldReplied.remove();
|
||||
}
|
||||
|
||||
} else {
|
||||
if (container.children[currentIndex]) {
|
||||
container.insertBefore(newChild, container.children[currentIndex]);
|
||||
} else {
|
||||
container.appendChild(newChild);
|
||||
}
|
||||
}
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
while (container.children.length > newChildren.length) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
|
||||
let targetScrollTop = 0;
|
||||
if (wasAtBottom) {
|
||||
|
|
@ -2569,14 +2738,127 @@ function setupChatScrollListener() {
|
|||
|
||||
|
||||
|
||||
async function downloadAndRenderAttachment(msgId, attId, type, name) {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
|
||||
let container = document.getElementById(`att-container-${msgId}-${attId}`);
|
||||
if (!container) return;
|
||||
|
||||
if (container.dataset.loaded === "true") {
|
||||
if (!type.startsWith("image/") && !type.startsWith("video/")) {
|
||||
let a = document.createElement("a");
|
||||
a.href = container.dataset.url;
|
||||
a.download = name;
|
||||
a.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (container.dataset.loading === "true") return;
|
||||
container.dataset.loading = "true";
|
||||
|
||||
let originalHtml = container.innerHTML;
|
||||
container.innerHTML = `<div style="padding: 1rem; color: var(--text-color);"><svg class="spinner" viewBox="0 0 50 50" style="width:24px;height:24px;"><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg></div>`;
|
||||
|
||||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
|
||||
let res = await fetch(`${url}/dm/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, {
|
||||
headers: {
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to fetch attachment");
|
||||
|
||||
let encryptedBytes = new Uint8Array(await res.arrayBuffer());
|
||||
let decryptedBytes = await decryptAesGcmFromBytes(encryptedBytes, dmKeyBytes);
|
||||
|
||||
let blob = new Blob([decryptedBytes], {type: type});
|
||||
let blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
container.dataset.loaded = "true";
|
||||
container.dataset.url = blobUrl;
|
||||
|
||||
if (type.startsWith("image/")) {
|
||||
container.innerHTML = `<img src="${blobUrl}" style="max-width: 100%; max-height: 300px; display: block;" onclick="window.open('${blobUrl}', '_blank')">`;
|
||||
} else if (type.startsWith("video/")) {
|
||||
container.innerHTML = `<video src="${blobUrl}" controls style="max-width: 100%; max-height: 300px; display: block;"></video>`;
|
||||
} else {
|
||||
container.innerHTML = originalHtml;
|
||||
container.style.border = "1px solid var(--accent-color)";
|
||||
let a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
a.download = name;
|
||||
a.click();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
container.innerHTML = `<div style="padding: 1rem; color: var(--big-red); font-size: 0.8rem;">Error loading</div>`;
|
||||
container.dataset.loading = "false";
|
||||
}
|
||||
}
|
||||
|
||||
let currentDraftAttachments = [];
|
||||
let replyingToMsgId = null;
|
||||
|
||||
function handleAttachmentSelection(event) {
|
||||
if (!event.target.files) return;
|
||||
for (let file of event.target.files) {
|
||||
currentDraftAttachments.push(file);
|
||||
}
|
||||
event.target.value = '';
|
||||
renderAttachmentsPreview();
|
||||
}
|
||||
|
||||
function removeDraftAttachment(index) {
|
||||
currentDraftAttachments.splice(index, 1);
|
||||
renderAttachmentsPreview();
|
||||
}
|
||||
|
||||
function renderAttachmentsPreview() {
|
||||
let previewDiv = document.getElementById("attachments-preview");
|
||||
if (!previewDiv) return;
|
||||
|
||||
if (currentDraftAttachments.length === 0) {
|
||||
previewDiv.style.display = "none";
|
||||
previewDiv.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
previewDiv.style.display = "flex";
|
||||
let html = "";
|
||||
currentDraftAttachments.forEach((file, index) => {
|
||||
let isImage = file.type.startsWith("image/");
|
||||
let isVideo = file.type.startsWith("video/");
|
||||
let iconHtml = "";
|
||||
if (isImage) {
|
||||
iconHtml = `<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
|
||||
} else if (isVideo) {
|
||||
iconHtml = `<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>`;
|
||||
} else {
|
||||
iconHtml = `<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div style="display: flex; align-items: center; gap: 0.8rem; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: var(--border-width) solid rgba(255, 255, 255, 0.1); padding: 0.5rem 0.5rem 0.5rem 1rem; border-radius: 0.75rem; flex-shrink: 0; max-width: 15.625rem; box-shadow: 0 0.25rem 0.375rem rgba(0,0,0,0.1); transition: transform 0.2s ease;">
|
||||
<div style="flex-shrink: 0; color: var(--accent-color); display: flex; align-items: center;">${iconHtml}</div>
|
||||
<div style="font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-color); font-weight: 500; flex-grow: 1;">${escapeHtml(file.name)}</div>
|
||||
<button onclick="removeDraftAttachment(${index})" style="background: transparent; color: var(--text-color); border: none; width: 1.5rem; height: 1.5rem; flex-shrink: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0.125rem; opacity: 0.6; transition: opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.6'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" style="width:100%; height:100%;"><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>`;
|
||||
});
|
||||
previewDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) {
|
||||
if (!currentDmId || !currentDmKey) return;
|
||||
|
||||
let input = document.getElementById("chat-input");
|
||||
let content = input.value.trim();
|
||||
if (!content) return;
|
||||
if (!content && currentDraftAttachments.length === 0) return;
|
||||
|
||||
let pendingMsgId = "pending_" + Date.now();
|
||||
let container = document.getElementById("chat-messages");
|
||||
|
|
@ -2596,17 +2878,67 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
try {
|
||||
let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey));
|
||||
let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes);
|
||||
|
||||
let attachmentsPayload = [];
|
||||
if (currentDraftAttachments.length > 0) {
|
||||
showAction(processBlah("action.attachment.uploading"), "uploading_atts");
|
||||
for (let i = 0; i < currentDraftAttachments.length; i++) {
|
||||
let file = currentDraftAttachments[i];
|
||||
let arrayBuffer = await file.arrayBuffer();
|
||||
let encryptedFileBytes = await encryptAesGcmToBytes(new Uint8Array(arrayBuffer), dmKeyBytes);
|
||||
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${url}/dm/attachment/staging?id=${id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
||||
},
|
||||
body: encryptedFileBytes
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error("error.network.error");
|
||||
}
|
||||
|
||||
if (res.status === 413) {
|
||||
throw new Error("error.body.too.large");
|
||||
}
|
||||
let text = await res.text();
|
||||
let decText;
|
||||
try {
|
||||
decText = await decryptString(text, passwordHash);
|
||||
} catch (e) {
|
||||
throw new Error("error:message.send.failed"); // trigger proper blah notification
|
||||
}
|
||||
|
||||
if (decText && decText.startsWith("success:")) {
|
||||
attachmentsPayload.push({
|
||||
id: i,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
staging_id: decText.split(":")[1]
|
||||
});
|
||||
} else {
|
||||
throw new Error(decText && decText.startsWith("error:") ? decText : "error:message.send.failed");
|
||||
}
|
||||
}
|
||||
clearAction("uploading_atts");
|
||||
}
|
||||
|
||||
let msgPayload = {
|
||||
string1: currentDmId,
|
||||
string2: encryptedContent,
|
||||
string3: "",
|
||||
string3: attachmentsPayload.length > 0 ? JSON.stringify(attachmentsPayload) : "",
|
||||
string4: replyingToMsgId || ""
|
||||
};
|
||||
|
||||
input.value = "";
|
||||
input.style.height = '2.5rem';
|
||||
sendbutton.style.height = '2.5rem';
|
||||
currentDraftAttachments = [];
|
||||
renderAttachmentsPreview();
|
||||
|
||||
window.forceScrollToBottom = true;
|
||||
replyingToMsgId = null;
|
||||
|
|
@ -2625,12 +2957,20 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
let parts = res.split(":");
|
||||
if (parts.length > 1 && parts[1] !== "message.sent") {
|
||||
let newMsgId = parts[1];
|
||||
let finalAttPayload = "";
|
||||
if (attachmentsPayload.length > 0) {
|
||||
finalAttPayload = JSON.stringify(attachmentsPayload.map(a => {
|
||||
let clone = Object.assign({}, a);
|
||||
delete clone.staging_id;
|
||||
return clone;
|
||||
}));
|
||||
}
|
||||
loadedMessages[newMsgId] = {
|
||||
author: id,
|
||||
timestamp: Date.now().toString(),
|
||||
type: "larp.text",
|
||||
content: encryptedContent,
|
||||
attachment: "",
|
||||
attachment: finalAttPayload,
|
||||
key: "0",
|
||||
pervious: "",
|
||||
responded: replyingToMsgId || "",
|
||||
|
|
@ -2640,9 +2980,14 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
clearAction("uploading_atts");
|
||||
clearAction("msgsending");
|
||||
console.error(e);
|
||||
showBlahNotification("error:message.send.failed");
|
||||
if (e.message && e.message.startsWith("error:")) {
|
||||
showBlahNotification(e.message);
|
||||
} else {
|
||||
showBlahNotification("error:message.send.failed");
|
||||
}
|
||||
let pElem = document.getElementById(pendingMsgId);
|
||||
if (pElem) pElem.remove();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,9 +141,16 @@ var chatScreen = `
|
|||
<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;">
|
||||
<div id="attachments-preview" style="display: none; padding: 0.5rem 1rem; gap: 0.8rem; overflow-x: auto; border-top: var(--border-width) solid var(--light-border-color); background: var(--main-bg-color); align-items: center; flex-shrink: 0;"></div>
|
||||
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem; align-items: flex-end; flex-shrink: 0;">
|
||||
<button id="chat-add-attachment" class="submit-button" onclick="clickAddAttachment(this, event)" style="margin: 0; padding: 0; width: 2.5rem; flex-shrink: 0; min-height: 2.5rem; max-height: 8rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24" fill="none" stroke="var(--main-bg-color)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<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 id="chat-sendmessage" class="submit-button" onclick="sendMessage(this)" style="margin: 0; padding: 0; width: 2.5rem; min-height: 2.5rem; max-height: 8rem;">
|
||||
<button id="chat-sendmessage" class="submit-button" onclick="sendMessage(this)" style="margin: 0; padding: 0; width: 2.5rem; flex-shrink: 0; min-height: 2.5rem; max-height: 8rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 16 16" fill="none">
|
||||
<path stroke="var(--main-bg-color)" stroke-linecap="round" stroke-linejoin="round" d="M 4.65 8 l -1.875 5.625 l 11.25 -5.625 L 2.775 2.375 l 1.875 5.625 z m 0 0 h 3.75" stroke-width="1.1"></path>
|
||||
</svg>
|
||||
|
|
@ -231,6 +238,28 @@ var profileContextMenu = `
|
|||
<span class="blah" style="color: var(--big-red)">title.logout</span>
|
||||
</button>
|
||||
`;
|
||||
var attachmentMenu = `
|
||||
<button onclick="document.getElementById('attach-take-photo').click(); fixedContextMenu.classList.remove('show');">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 0 24 24" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
|
||||
<span class="blah">action.attachment.take.photo</span>
|
||||
</button>
|
||||
<button onclick="document.getElementById('attach-media').click(); fixedContextMenu.classList.remove('show');">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 0 24 24" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
||||
<span class="blah">action.attachment.media</span>
|
||||
</button>
|
||||
<button onclick="document.getElementById('attach-file').click(); fixedContextMenu.classList.remove('show');">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 0 24 24" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||
<span class="blah">action.attachment.file</span>
|
||||
</button>
|
||||
<button onclick="console.log('poll'); fixedContextMenu.classList.remove('show');">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 0 24 24" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
|
||||
<span class="blah">action.attachment.poll</span>
|
||||
</button>
|
||||
<input type="file" id="attach-take-photo" accept="image/*" capture="environment" style="display: none;" onchange="handleAttachmentSelection(event)" multiple>
|
||||
<input type="file" id="attach-media" accept="image/*,video/*" style="display: none;" onchange="handleAttachmentSelection(event)" multiple>
|
||||
<input type="file" id="attach-file" style="display: none;" onchange="handleAttachmentSelection(event)" multiple>
|
||||
`;
|
||||
|
||||
var addSpaceMenu = `
|
||||
<button onclick="gotoJoinSpace()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue