Files
objects/wco/privatri/privatri.js
2025-08-11 09:26:06 +02:00

506 lines
20 KiB
JavaScript

import apx from "./apx.js";
import { getOldestPrivatriids, syncronizeBackend } from "./utils.js";
const bodyEl = document.querySelector("body");
const searchInputEl = document.querySelector("#threadSearchBar");
const threadFilterOptionsElArray = document.querySelector("#threadFilterOptions").querySelectorAll("li");
const threadsContainerEl = document.querySelector("#threadsContainer");
const threadPageEl = document.querySelector("#threadPage");
const messageInputEl = document.getElementById("messageInput");
const messagesContainerEl = document.querySelector("#messagesContainer");
const attachmentsInputEl = document.querySelector("#attachmentsInput");
function formatDate(timestamp) {
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
searchInputEl.addEventListener("input", () => {
const value = searchInputEl.value.toLowerCase();
Array.from(threadsContainerEl.children).forEach(child => {
if (child.querySelector("li > div").textContent.toLowerCase().includes(value)) {
child.style.display = "block";
} else {
child.style.display = "none";
};
});
});
threadFilterOptionsElArray.forEach(option => {
option.addEventListener("click", async () => {
const filter = option.getAttribute("data-filter");
if (filter === "date") {
Array.from(threadsContainerEl.children).sort((a, b) => {
return a.querySelector("li > span").getAttribute("data-timestamp") - b.querySelector("li > span").getAttribute("data-timestamp");
}).forEach(child => {
threadsContainerEl.appendChild(child);
});
} else if (filter === "name") {
Array.from(threadsContainerEl.children).sort((a, b) => {
return a.querySelector("li > div").textContent.localeCompare(b.querySelector("li > div").textContent);
}).forEach(child => {
threadsContainerEl.appendChild(child);
});
};
});
});
const sendNewMessage = async (timestamp, message, attachmentsArray) => {
let messageTemplate = "";
await fetch("./message.mustache")
.then(res => res.text())
.then(template => {
messageTemplate = template;
});
messageTemplate = Mustache.render(messageTemplate, { timestamp, message, date: formatDate(timestamp) });
const tempNode = document.createElement("div");
tempNode.innerHTML = messageTemplate;
const messageNode = tempNode.firstElementChild;
attachmentsArray.forEach(attachment => {
messageNode.querySelector("div.attachmentsContainer").insertAdjacentHTML("beforeend", `<img onclick="attachEventDisplayAttachment(this)" class="w-20 h-20 object-cover rounded" src="data:${attachment.fileType};base64,${attachment.content}" alt="${attachment.filename}"/>`);
});
return messageNode.outerHTML;
};
const editMessage = async (timestamp, message) => {
let editMessageTemplate = "";
await fetch("./editMessage.mustache")
.then(res => res.text())
.then(template => {
editMessageTemplate = template;
});
return Mustache.render(editMessageTemplate, { message, date: formatDate(timestamp) });
};
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
sendMessageBtn.addEventListener("click", async () => {
const message = messageInputEl.value.trim();
if (message !== "") {
const messageObj = await (async (publicKey) => {
const timestamp = Date.now();
const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey);
return {
privatriid: `${threadPageEl.getAttribute("data-uuid")}_${timestamp}`,
timestamp: timestamp,
sender_alias: alias,
message: await apx.crypto.encryptMessage(message, publicKey),
attachments: await (async () => {
const attachmentsArray = [];
const localAttachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || [];
if (localAttachmentsArray.length > 0) {
for (const attachment of localAttachmentsArray) {
attachmentsArray.push(
{
fileType: attachment.fileType,
filename: attachment.filename,
content: await apx.crypto.encryptMessage(attachment.content, publicKey)
}
);
};
};
return attachmentsArray;
})()
};
})((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messagesContainerEl.firstElementChild.getAttribute("data-timestamp")}`)).publicKey);
const newMessageHTML = await sendNewMessage(messageObj.timestamp, message, JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []);
sessionStorage.removeItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`);
messagesContainerEl.insertAdjacentHTML("beforeend", newMessageHTML);
messageInputEl.value = "";
// Faire un post sur l'endpoint /privatri
await apx.indexedDB.set("privatri", "messages", messageObj);
};
});
document.addEventListener("DOMContentLoaded", async () => {
const lastConnection = JSON.parse(localStorage.getItem("lastConnection")) || Date.now();
await syncronizeBackend(lastConnection);
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
const thread = async (name, uuid) => {
let threadTemplate = "";
await fetch("./thread.mustache")
.then(res => res.text())
.then(template => {
threadTemplate = template;
});
return Mustache.render(threadTemplate, { uuid, name });
};
for (const privatriid of privatriidArray) {
const obj = await apx.indexedDB.get("privatri", "messages", privatriid)
const privateKey = (await apx.indexedDB.get("privatri", "threads", obj.thread)).privateKey;
const name = (await apx.crypto.decryptMessage(obj.title, privateKey)).data;
threadsContainerEl.insertAdjacentHTML("beforeend", await thread(name, obj.thread));
};
Array.from(threadsContainerEl.children).forEach(child => {
child.addEventListener("click", async () => {
const uuid = child.getAttribute("data-uuid");
const messagesObj = await new Promise((resolve, reject) => {
const request = indexedDB.open("privatri", 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction("messages", "readonly");
const store = transaction.objectStore("messages");
const cursorRequest = store.openCursor();
const result = [];
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const obj = cursor.value;
const keyUuid = obj.privatriid.split("_")[0];
if (keyUuid === uuid) {
result.push(obj);
};
cursor.continue();
} else {
resolve(result);
};
};
cursorRequest.onerror = (event) => reject(event);
};
request.onerror = (event) => reject(event);
});
threadPageEl.setAttribute("data-uuid", uuid);
threadPageEl.querySelector("#threadName").innerText = (await apx.crypto.decryptMessage(messagesObj[0].title, (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey)).data;
messagesContainerEl.innerHTML = "";
let privateKey = "";
for (const message of messagesObj) {
if (privateKey === "") {
privateKey = (await apx.indexedDB.get("privatri", "threads", message.thread)).privateKey;
};
const decryptedMessage = (await apx.crypto.decryptMessage(message.message, privateKey)).data;
let decryptedAttachments = [];
if (message.attachments !== undefined) {
decryptedAttachments = await (async () => {
const attachmentsArray = [];
if (message.attachments.length > 0) {
for (const attachment of message.attachments) {
attachmentsArray.push({
fileType: attachment.fileType,
filename: attachment.filename,
content: (await apx.crypto.decryptMessage(attachment.content, privateKey)).data
});
};
};
return attachmentsArray;
})();
};
messagesContainerEl.insertAdjacentHTML("beforeend", await sendNewMessage(message.timestamp, decryptedMessage, decryptedAttachments));
};
});
});
document.querySelectorAll("a").forEach(link => {
link.addEventListener("click", async (event) => {
event.preventDefault();
window.history.replaceState({}, document.title, window.location.pathname);
const templateName = link.getAttribute("data-template");
if (templateName === "threadAliasList" || templateName === "threadSettings") {
window.history.pushState({}, "", `${window.location.pathname}?uuid=${threadPageEl.getAttribute("data-uuid")}`);
};
await fetch(`./${templateName}.mustache`)
.then(res => res.text())
.then(template => {
template = Mustache.render(template, {});
bodyEl.innerHTML = template;
});
const script = document.createElement("script");
script.type = "module";
script.src = `./${templateName}.js`;
bodyEl.appendChild(script);
});
});
});
window.addEventListener("beforeunload", () => {
if (apx)
localStorage.setItem("lastConnection", JSON.stringify(Date.now()));
});
window.attachDeleteMessageEvent = async function attachDeleteMessageEvent(btn) {
const messageEl = btn.parentElement.parentElement.parentElement;
const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey;
const signatureMessage = `${JSON.parse(localStorage.getItem("apx")).data.headers.xalias}_${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`;
const signature = await apx.crypto.sign(signatureMessage, privateKey);
let verified = false;
try {
verified = await fetch("", {
method: "GET",
body: JSON.stringify({
message: signatureMessage,
signature: signature,
uuid: threadPageEl.getAttribute("data-uuid"),
timestamp: messageEl.getAttribute("data-timestamp"),
})
});
} catch (error) {
console.error("Error while verifying signature:", error);
};
const authorAlias = await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, privateKey).data;
if ((JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) && (verified === true)) {
await apx.indexedDB.del("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageEl.remove();
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to delete this message."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
};
window.attachEditMessageEvent = async function attachEditMessageEvent(btn) {
const messageEl = btn.parentElement.parentElement.parentElement;
const authorAlias = (await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey)).data;
if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) {
const messageValue = messageEl.querySelector("div.message").innerText;
const attachmentsArray = (() => {
const attachmentsArray = [];
messageEl.querySelector("div.attachmentsContainer").querySelectorAll("img").forEach(img => {
attachmentsArray.push(
{
fileType: img.getAttribute("src").match(/^data:(.*);base64,/)[1],
filename: img.getAttribute("alt"),
content: img.getAttribute("src").split(",")[1]
}
);
});
return attachmentsArray;
})();
messageEl.innerHTML = await editMessage(parseInt(messageEl.getAttribute("data-timestamp"), 10), messageEl.querySelector("div.message").innerText);
messageEl.querySelector("button.cancelEditBtn").addEventListener("click", async () => {
const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, messageValue, attachmentsArray);
});
messageEl.querySelector("button.saveEditBtn").addEventListener("click", async () => {
const newMessageValue = messageEl.querySelector("textarea").value.trim();
if (newMessageValue !== "") {
const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey;
const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageObj.message = await apx.crypto.encryptMessage(newMessageValue, privateKey),
await apx.indexedDB.set("privatri", "messages", messageObj);
messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, newMessageValue, attachmentsArray);
};
});
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this message."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
};
window.attachEventDisplayAttachment = function(img) {
let overlay = document.getElementById("imageOverlay");
if (overlay === null) {
overlay = document.createElement('div');
overlay.id = "imageOverlay";
overlay.style.position = "fixed";
overlay.style.top = 0;
overlay.style.left = 0;
overlay.style.width = "100vw";
overlay.style.height = "100vh";
overlay.style.background = "rgba(0, 0, 0, 0.75)";
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.zIndex = 999;
overlay.onclick = () => {
overlay.remove();
};
document.body.appendChild(overlay);
} else {
overlay.innerHTML = "";
overlay.style.display = "flex";
};
const fullScreenImage = document.createElement("img");
fullScreenImage.src = img.src;
fullScreenImage.alt = img.alt;
fullScreenImage.style.maxWidth = "90vw";
fullScreenImage.style.maxHeight = "90vh";
overlay.appendChild(fullScreenImage);
};
document.querySelector("#attachmentsBtn").addEventListener("click", () => {
attachmentsInputEl.click();
});
attachmentsInputEl.addEventListener("change", async () => {
const filesArray = Array.from(attachmentsInputEl.files);
const maxFileSize = 5 * 1024 * 1024;
const maxSize = 512;
for (const file of filesArray) {
if (file.size <= maxFileSize) {
const attachmentObj = await new Promise((resolve, reject) => {
const attachmentObj = {
fileType: file.type,
filename: file.name,
content: ""
};
const img = new Image();
img.onload = () => {
if (img.width > maxSize || img.height > maxSize) {
let width = img.width;
let height = img.height;
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
};
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
};
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(blob => {
const reader = new FileReader();
reader.onload = (event) => {
attachmentObj.content = event.target.result.split(",")[1];
resolve(attachmentObj);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
}, "image/jpeg", 0.8);
} else {
const reader = new FileReader();
reader.onload = (event) => {
attachmentObj.content = event.target.result.split(",")[1];
resolve(attachmentObj);
};
reader.onerror = reject;
reader.readAsDataURL(file);
};
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
return attachmentObj;
});
if (sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`) === null) {
sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify([attachmentObj]));
} else {
const attachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`));
attachmentsArray.push(attachmentObj);
sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify(attachmentsArray));
};
};
};
});