506 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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));
 | |
|             };
 | |
|         };
 | |
|     };
 | |
| }); |