Add privatri wco

This commit is contained in:
devpotatoes
2025-08-11 09:26:06 +02:00
parent e4501f112e
commit 87aaf3fa19
21 changed files with 4106 additions and 181 deletions

View File

@@ -58,4 +58,53 @@ apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => {
);
};
apx.crypto.sign = async (message, privateKey) => {
privateKey = await openpgp.readPrivateKey(
{
armoredKey: privateKey
}
);
return await openpgp.sign(
{
message: await openpgp.createMessage(
{
text: message
}
),
signingKeys: privateKey
}
);
};
apx.crypto.verifySignature = async (message, signature, publicKey) => {
publicKey = await openpgp.readKey(
{
armoredKey: publicKey
}
);
const verified = await openpgp.verify(
{
message: await openpgp.createMessage(
{
text: message
}
),
signature: await openpgp.readSignature(
{
armoredSignature: signature
}
),
verificationKeys: publicKey
}
);
if (await verified.signatures[0].verified) {
return true;
} else {
return false;
};
};
export default apx;

View File

@@ -0,0 +1,6 @@
<ul class="list">
<li class="list-row items-center">
<div class="text-xl font-medium">{{alias.decrypted}}</div>
<button class="removeAliasBtn btn btn-neutral w-fit rounded-lg ml-auto" data-alias="{{alias.crypted}}">Remove this alias</button>
</li>
</ul>

194
wco/privatri/apx.js Normal file
View File

@@ -0,0 +1,194 @@
var apx = apx || {};
apx.crypto = apx.crypto || {};
apx.indexedDB = apx.indexedDB || {};
apx.crypto.genKey = async (uuid) => {
return await openpgp.generateKey(
{
type: "ecc",
curve: "curve25519",
userIDs: [
{
alias: uuid
}
],
passphrase: "",
format: "armored",
}
);
};
apx.crypto.encryptMessage = async (message, publicKey) => {
publicKey = await openpgp.readKey(
{
armoredKey: publicKey
}
);
return await openpgp.encrypt(
{
message: await openpgp.createMessage(
{
text: message
}
),
encryptionKeys: publicKey
}
);
};
apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => {
privateKey = await openpgp.readPrivateKey(
{
armoredKey: privateKey
}
);
const message = await openpgp.readMessage(
{
armoredMessage: encryptedMessage
}
);
return await openpgp.decrypt(
{
message,
decryptionKeys: privateKey
}
);
};
apx.crypto.sign = async (message, privateKey) => {
privateKey = await openpgp.readPrivateKey(
{
armoredKey: privateKey
}
);
return await openpgp.sign(
{
message: await openpgp.createMessage(
{
text: message
}
),
signingKeys: privateKey
}
);
};
apx.crypto.verifySignature = async (message, signature, publicKey) => {
publicKey = await openpgp.readKey(
{
armoredKey: publicKey
}
);
const verified = await openpgp.verify(
{
message: await openpgp.createMessage(
{
text: message
}
),
signature: await openpgp.readSignature(
{
armoredSignature: signature
}
),
verificationKeys: publicKey
}
);
if (await verified.signatures[0].verified) {
return true;
} else {
return false;
};
};
apx.indexedDB.set = async (db, storeName, value) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("threads")) {
db.createObjectStore("threads", { keyPath: "uuid" });
};
if (!db.objectStoreNames.contains("messages")) {
db.createObjectStore("messages", { keyPath: "privatriid" });
};
};
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const putRequest = store.put(value);
putRequest.onsuccess = () => resolve();
putRequest.onerror = (error) => reject(error);
};
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.get = async (db, storeName, key) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve(null);
}
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const getRequest = store.get(key);
getRequest.onsuccess = () => {
resolve(getRequest.result || null);
};
getRequest.onerror = () => resolve(null);
};
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.del = async (db, storeName, key) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const deleteRequest = store.delete(key);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = (error) => reject(error);
};
request.onerror = (error) => reject(error);
});
};
export default apx;

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,44 @@
import apx from "./apx.js";
const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn");
autoDeletionBtnElArray.forEach(btn => {
btn.addEventListener("click", () => {
autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200"));
btn.classList.add("bg-base-200");
});
});
document.querySelector("#createThreadBtn").addEventListener("click", async () => {
const { publicKey, privateKey } = await apx.crypto.genKey();
const messageObj = await (async (publicKey) => {
const uuid = crypto.randomUUID();
const timestamp = Date.now();
const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey);
return {
privatriid: `${uuid}_${timestamp}`,
thread: uuid,
timestamp: timestamp,
owner: alias,
title: await apx.crypto.encryptMessage(document.querySelector("#threadNameInput").value, publicKey),
sender_alias: alias,
publicKey: publicKey,
aliases: [
alias
],
message: await apx.crypto.encryptMessage(document.querySelector("#threadDescriptionInput").value, publicKey),
dt_autodestruction: (() => {
const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200"));
return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10);
})(),
urgencydeletion: document.querySelector('input[type="checkbox"].toggle').checked
};
})(publicKey);
// Faire un post sur l'endpoint /privatri
await apx.indexedDB.set("privatri", "threads", { uuid: messageObj.thread, privateKey: privateKey });
await apx.indexedDB.set("privatri", "messages", messageObj);
});

View File

@@ -0,0 +1,34 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit min-w-xs">
<h1 class="text-4xl">New thread</h1>
<div class="input rounded-lg w-full flex items-center gap-x-2 focus-within:outline focus-within:outline-2 focus-within:outline-primary bg-base-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="fill-current text-base-content">
<path d="M80 0v-160h800V0H80Zm160-320h56l312-311-29-29-28-28-311 312v56Zm-80 80v-170l448-447q11-11 25.5-17t30.5-6q16 0 31 6t27 18l55 56q12 11 17.5 26t5.5 31q0 15-5.5 29.5T777-687L330-240H160Zm560-504-56-56 56 56ZM608-631l-29-29-28-28 57 57Z"/>
</svg>
<input id="threadNameInput" type="text" placeholder="Thread name..." value="{threadName}" class="flex-1 bg-transparent border-none outline-none shadow-none"/>
</div>
<textarea id="threadDescriptionInput" class="textarea rounded-lg" placeholder="Description..."></textarea>
<div class="dropdown dropdown-center w-full">
<div tabindex="0" role="button" class="btn w-full rounded-lg flex items-center justify-between">
Message auto destruction
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="fill-current text-base-content">
<path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box w-full z-1 w-52 p-2 shadow-sm">
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="86400000">
<a class="rounded-lg">Every days</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="604800000">
<a class="rounded-lg">Every weeks</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="2592000000">
<a class="rounded-lg">Every months</a>
</li>
</ul>
</div>
<div class="w-full flex items-center justify-left gap-x-4">
<input type="checkbox" class="toggle"/>
<p>Urgency deletion</p>
</div>
<button id="createThreadBtn" class="btn w-full rounded-lg mt-16">Create</button>
</div>

View File

@@ -0,0 +1,16 @@
<textarea class="message textarea rounded-lg">{{message}}</textarea>
<div class="flex">
<span class="text-base-content opacity-50 text-sm ml-4">{{date}}</span>
<div class="flex badge rounded-lg w-20 ml-4 gap-0 p-0">
<button class="cancelEditBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tl-lg rounded-bl-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="m336-280 144-144 144 144 56-56-144-144 144-144-56-56-144 144-144-144-56 56 144 144-144 144 56 56ZM480-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>
</button>
<button class="saveEditBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tr-lg rounded-br-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/>
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,52 @@
const url = "https://www.facebook.com/";
const bodyEl = document.querySelector("body");
const qrCodeCanvasEl = document.querySelector("#qrCodeCanvas");
const invitationLinkInputEl = document.querySelector("#invitationLinkInput");
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
(async () => {
const qrCode = new QRCodeStyling({
width: 425,
height: 425,
type: "svg",
data: url,
image: "./assets/icon.png",
dotsOptions: {
color: "#ffffff",
type: "rounded"
},
backgroundOptions: {
color: getComputedStyle(bodyEl).backgroundColor || "#000000",
},
imageOptions: {
crossOrigin: "anonymous",
margin: 20
}
});
qrCode.append(qrCodeCanvasEl);
invitationLinkInputEl.value = url;
copyBtn.addEventListener("click", async () => {
navigator.clipboard.writeText(invitationLinkInputEl.value);
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("Invitation link copied to clipboard."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
});
})();

View File

@@ -0,0 +1,12 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit">
<h1 class="text-4xl">Invite an alias into this thread</h1>
<div id="qrCodeCanvas"></div>
<div class="input rounded-lg flex items-center w-full pr-0 gap-x-2 bg-base-100">
<input id="invitationLinkInput" type="text" placeholder="Url" class="flex-1 bg-transparent border-none outline-none shadow-none" readonly/>
<button id="copyBtn" type="button" class="p-2 rounded-lg fill-current text-base-content">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
<path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z" />
</svg>
</button>
</div>
</div>

108
wco/privatri/main_fr.html Normal file
View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./output.css">
<script type="module" src="./privatri.js" defer></script>
<script src="https://unpkg.com/mustache@latest/mustache.min.js"></script>
<script src="https://unpkg.com/openpgp@latest/dist/openpgp.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/qr-code-styling@1.5.0/lib/qr-code-styling.js"></script>
<title>Document</title>
</head>
<body class="bg-base-100 flex items-center justify-center h-screen w-screen ">
<section class="w-fit min-w-110 h-screen max-h-screen bg-base-100 border-r border-solid border-r-base-300 relative flex flex-col">
<div class="w-full h-12 max-h-12 flex items-center justify-evenly mt-4 mb-4 flex-shrink-0">
<div class="dropdown h-full">
<div tabindex="0" role="button" class="h-full flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li>
<a class="rounded-lg">Notifications</a>
</li>
<li>
<a class="rounded-lg" data-template="createThread">New thread</a>
</li>
<li>
<a class="rounded-lg">Account</a>
</li>
</ul>
</div>
<label class="input rounded-lg md-4">
<svg class="h-4 w-4 text-base-content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input id="threadSearchBar" type="search" required placeholder="Search for threads" class="flex-1 bg-transparent outline-none placeholder:opacity-75 placeholder:text-base-content text-base-content" />
</label>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</div>
<ul id="threadFilterOptions" tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li data-filter="date">
<p class="rounded-lg">Sort by date</p>
</li>
<li data-filter="name">
<p class="rounded-lg">Sort by name</p>
</li>
</ul>
</div>
</div>
<div id="threadsContainer" class="flex-1 overflow-y-auto flex flex-col"></div>
<a class="w-16 h-16 btn bg-base-content rounded-full p-0 shadow-lg flex items-center justify-center absolute bottom-8 right-8" data-template="createThread">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-100">
<path d="M120-160v-600q0-33 23.5-56.5T200-840h480q33 0 56.5 23.5T760-760v203q-10-2-20-2.5t-20-.5q-10 0-20 .5t-20 2.5v-203H200v400h283q-2 10-2.5 20t-.5 20q0 10 .5 20t2.5 20H240L120-160Zm160-440h320v-80H280v80Zm0 160h200v-80H280v80Zm400 280v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-360v-400 400Z"/>
</svg>
</a>
</section>
<section id="threadPage" class="w-full flex-1 bg-base-100 flex flex-col h-screen">
<div class="w-full h-12 max-h-12 mt-4 mb-4 flex items-center justify-center pl-8 pr-8">
<div class="flex-1 flex items-center justify-center">
<span id="threadName" class="text-xl">{threadName}</span>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm">
<li>
<a class="rounded-lg" data-template="inviteAlias">Invite an alias</a>
</li>
<li>
<a class="rounded-lg" data-template="threadAliasList">See all alias</a>
</li>
<li>
<a class="rounded-lg" data-template="threadSettings">Settings</a>
</li>
</ul>
</div>
</div>
<div id="messagesContainer" class="w-full flex-1 min-h-0 bg-base-200 overflow-y-auto flex flex-col p-4 gap-y-6"></div>
<div class="flex items-center justify-center w-full h-fit pt-4 pb-4 pl-8 pr-8 gap-x-4">
<button id="attachmentsBtn" class="hover:bg-base-200 transition rounded-full p-2 flex items-center justify-center">
<input id="attachmentsInput" type="file" multiple accept="image/png, image/jpeg" class="hidden"/>
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z"/>
</svg>
</button>
<div class="flex items-center rounded-full w-full h-10 bg-base-300">
<input id="messageInput" type="text" placeholder="Write a message..." class="flex-1 bg-transparent border-transparent outline-none px-4" />
<button id="sendMessageBtn" type="button" class="flex justify-center items-center ml-2 p-2 rounded-full hover:bg-base-200 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M120-160v-640l760 320-760 320Zm80-120 474-200-474-200v140l240 60-240 60v140Zm0 0v-400 400Z" />
</svg>
</button>
</div>
</section>
</body>
</html>

View File

@@ -1,8 +1,93 @@
<div id="privatri-container">
<!-- Le contenu du composant privatri sera rendu ici -->
<h2>Messages Privés (privatri)</h2>
<p>Ce composant gère le stockage sécurisé des messages dans le navigateur.</p>
<div id="privatri-threads">
<!-- La liste des fils de discussion pourrait être affichée ici -->
</div>
</div>
<section class="w-fit min-w-110 h-screen max-h-screen bg-base-100 border-r border-solid border-r-base-300 relative flex flex-col">
<div class="w-full h-12 max-h-12 flex items-center justify-evenly mt-4 mb-4 flex-shrink-0">
<div class="dropdown h-full">
<div tabindex="0" role="button" class="h-full flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li>
<a href="./notificationsMenu.html" class="rounded-lg">Notifications</a>
</li>
<li>
<a href="./newMenu3.html" class="rounded-lg">New thread</a>
</li>
<li>
<a class="rounded-lg">Account</a>
</li>
</ul>
</div>
<label class="input rounded-lg md-4">
<svg class="h-4 w-4 text-base-content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input id="threadSearchBar" type="search" required placeholder="Search for threads" class="flex-1 bg-transparent outline-none placeholder:opacity-75 placeholder:text-base-content text-base-content" />
</label>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</div>
<ul id="threadFilterOptions" tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li data-filter="date">
<p class="rounded-lg">Sort by date</p>
</li>
<li data-filter="name">
<a class="rounded-lg">Sort by name</a>
</li>
</ul>
</div>
</div>
<div id="threadsContainer" class="flex-1 overflow-y-auto flex flex-col"></div>
<a href="./newMenu3.html" class="w-16 h-16 btn bg-base-content rounded-full p-0 shadow-lg flex items-center justify-center absolute bottom-8 right-8">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-100">
<path d="M120-160v-600q0-33 23.5-56.5T200-840h480q33 0 56.5 23.5T760-760v203q-10-2-20-2.5t-20-.5q-10 0-20 .5t-20 2.5v-203H200v400h283q-2 10-2.5 20t-.5 20q0 10 .5 20t2.5 20H240L120-160Zm160-440h320v-80H280v80Zm0 160h200v-80H280v80Zm400 280v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-360v-400 400Z"/>
</svg>
</a>
</section>
<section id="threadPage" class="w-full flex-1 bg-base-100 flex flex-col h-screen">
<div class="w-full h-12 max-h-12 mt-4 mb-4 flex items-center justify-center pl-8 pr-8">
<div class="flex-1 flex items-center justify-center">
<span id="threadName" class="text-xl">{threadName}</span>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm">
<li>
<a href="./inviteAllias.html" class="rounded-lg">Invite an alias</a>
</li>
<li>
<a id="aliasListHyperlink" class="rounded-lg">See all alias</a>
</li>
<li>
<a id="settingsHyperlink" class="rounded-lg">Settings</a>
</li>
</ul>
</div>
</div>
<div id="messagesContainer" class="w-full flex-1 min-h-0 bg-base-200 overflow-y-auto flex flex-col p-4 gap-y-6"></div>
<div class="flex items-center justify-center w-full h-fit pt-4 pb-4 pl-8 pr-8 gap-x-4">
<button id="attachmentsBtn" class="hover:bg-base-200 transition rounded-full p-2 flex items-center justify-center">
<input id="attachmentsInput" type="file" multiple accept="image/png, image/jpeg" class="hidden"/>
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z"/>
</svg>
</button>
<div class="flex items-center rounded-full w-full h-10 bg-base-300">
<input id="messageInput" type="text" placeholder="Write a message..." class="flex-1 bg-transparent border-transparent outline-none px-4" />
<button id="sendMessageBtn" type="button" class="flex justify-center items-center ml-2 p-2 rounded-full hover:bg-base-200 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M120-160v-640l760 320-760 320Zm80-120 474-200-474-200v140l240 60-240 60v140Zm0 0v-400 400Z" />
</svg>
</button>
</div>
</section>

View File

@@ -0,0 +1,23 @@
<div data-timestamp="{{timestamp}}" class="group">
<div class="chat chat-start">
<div class="message chat-bubble rounded-lg text-base">
{{message}}
<div class="attachmentsContainer grid gap-2 mt-2 grid-cols-[repeat(auto-fit,minmax(5rem,1fr))] max-w-[20rem]"></div>
</div>
</div>
<div class="flex">
<span class="text-base-content opacity-50 text-sm ml-4">{{date}}</span>
<div class="hidden group-hover:flex badge rounded-lg w-20 ml-4 gap-0 p-0">
<button onclick="attachDeleteMessageEvent(this)" class="deleteMessageBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tl-lg rounded-bl-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<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>
</button>
<button onclick="attachEditMessageEvent(this)" class="editMessageBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tr-lg rounded-br-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/>
</svg>
</button>
</div>
</div>
</div>

2733
wco/privatri/output.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,194 +1,506 @@
/**
* @file privatri.js
* @description WCO component for managing private, thread-based messages in IndexedDB.
* @version 1.0
*/
import apx from "./apx.js";
import { getOldestPrivatriids, syncronizeBackend } from "./utils.js";
((window) => {
'use strict';
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");
// --- Component Definition ---
const privatri = {};
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");
// --- Private State ---
let _db = null; // Holds the single, persistent IndexedDB connection
const DB_NAME = 'privatriDB';
const DB_VERSION = 1;
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
/**
* Logs messages to the console with a consistent prefix.
* @param {string} level - The log level (e.g., 'log', 'error', 'warn').
* @param {...any} args - The messages to log.
*/
const _log = (level, ...args) => {
console[level]('[privatri]', ...args);
};
searchInputEl.addEventListener("input", () => {
const value = searchInputEl.value.toLowerCase();
/**
* Opens and initializes the IndexedDB database.
* This function is called by init() and handles the connection and schema upgrades.
* @param {string[]} initialStoreNames - An array of store names (thread IDs) to ensure exist.
* @returns {Promise<IDBDatabase>} A promise that resolves with the database connection.
*/
const _openDatabase = (initialStoreNames = []) => {
return new Promise((resolve, reject) => {
_log('log', `Opening database "${DB_NAME}" with version ${DB_VERSION}.`);
Array.from(threadsContainerEl.children).forEach(child => {
if (child.querySelector("li > div").textContent.toLowerCase().includes(value)) {
child.style.display = "block";
} else {
child.style.display = "none";
};
});
});
const request = indexedDB.open(DB_NAME, DB_VERSION);
threadFilterOptionsElArray.forEach(option => {
option.addEventListener("click", async () => {
const filter = option.getAttribute("data-filter");
// This event is only triggered for new databases or version changes.
request.onupgradeneeded = (event) => {
const db = event.target.result;
_log('log', 'Database upgrade needed. Current stores:', [...db.objectStoreNames]);
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);
});
};
});
});
initialStoreNames.forEach(storeName => {
if (!db.objectStoreNames.contains(storeName)) {
_log('log', `Creating new object store: "${storeName}"`);
db.createObjectStore(storeName, { keyPath: 'key' });
}
const sendNewMessage = async (timestamp, message, attachmentsArray) => {
let messageTemplate = "";
await fetch("./message.mustache")
.then(res => res.text())
.then(template => {
messageTemplate = template;
});
};
request.onsuccess = (event) => {
_log('log', 'Database opened successfully.');
_db = event.target.result;
messageTemplate = Mustache.render(messageTemplate, { timestamp, message, date: formatDate(timestamp) });
// Generic error handler for the connection
_db.onerror = (event) => {
_log('error', 'Database error:', event.target.error);
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();
};
resolve(_db);
};
document.body.appendChild(overlay);
} else {
overlay.innerHTML = "";
overlay.style.display = "flex";
};
request.onerror = (event) => {
_log('error', 'Failed to open database:', event.target.error);
reject(event.target.error);
};
});
};
const fullScreenImage = document.createElement("img");
// --- Public API ---
fullScreenImage.src = img.src;
fullScreenImage.alt = img.alt;
fullScreenImage.style.maxWidth = "90vw";
fullScreenImage.style.maxHeight = "90vh";
overlay.appendChild(fullScreenImage);
};
/**
* Initializes the privatri component.
* Opens the database connection and ensures initial object stores are created.
* @param {object} config - Configuration object.
* @param {string[]} [config.threads=[]] - An array of initial thread IDs (store names) to create.
* @returns {Promise<void>}
*/
privatri.init = async (config = {}) => {
if (_db) {
_log('warn', 'privatri component already initialized.');
return;
}
const threads = config.threads || [];
await _openDatabase(threads);
};
document.querySelector("#attachmentsBtn").addEventListener("click", () => {
attachmentsInputEl.click();
});
/**
* Retrieves a value from a specific store (thread).
* @param {string} storeName - The name of the store (thread ID).
* @param {string} key - The key of the item to retrieve.
* @returns {Promise<any|null>} A promise that resolves with the value or null if not found.
*/
privatri.getValue = (storeName, key) => {
return new Promise((resolve, reject) => {
if (!_db || !_db.objectStoreNames.contains(storeName)) {
_log('warn', `Store "${storeName}" does not exist.`);
return resolve(null);
}
const transaction = _db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
attachmentsInputEl.addEventListener("change", async () => {
const filesArray = Array.from(attachmentsInputEl.files);
const maxFileSize = 5 * 1024 * 1024;
const maxSize = 512;
request.onsuccess = () => resolve(request.result ? request.result.value : null);
request.onerror = (e) => {
_log('error', `Error getting value for key "${key}" from store "${storeName}":`, e.target.error);
reject(e.target.error);
};
});
};
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: ""
};
/**
* Adds or updates a key-value pair in a specific store (thread).
* @param {string} storeName - The name of the store (thread ID).
* @param {string} key - The key of the item to set.
* @param {any} value - The value to store.
* @returns {Promise<void>}
*/
privatri.setValue = (storeName, key, value) => {
return new Promise((resolve, reject) => {
if (!_db || !_db.objectStoreNames.contains(storeName)) {
_log('error', `Cannot set value. Store "${storeName}" does not exist.`);
return reject(new Error(`Store "${storeName}" not found.`));
}
const transaction = _db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put({ key: key, value: value });
const img = new Image();
request.onsuccess = () => resolve();
request.onerror = (e) => {
_log('error', `Error setting value for key "${key}" in store "${storeName}":`, e.target.error);
reject(e.target.error);
};
});
};
img.onload = () => {
if (img.width > maxSize || img.height > maxSize) {
let width = img.width;
let height = img.height;
/**
* Removes a key-value pair from a specific store (thread).
* @param {string} storeName - The name of the store (thread ID).
* @param {string} key - The key of the item to remove.
* @returns {Promise<void>}
*/
privatri.removeKey = (storeName, key) => {
return new Promise((resolve, reject) => {
if (!_db || !_db.objectStoreNames.contains(storeName)) {
_log('warn', `Cannot remove key. Store "${storeName}" does not exist.`);
return resolve(); // Resolve peacefully if store doesn't exist
}
const transaction = _db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
};
request.onsuccess = () => resolve();
request.onerror = (e) => {
_log('error', `Error removing key "${key}" from store "${storeName}":`, e.target.error);
reject(e.target.error);
};
});
};
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
};
/**
* A utility function to save a batch of messages, demonstrating how to use the component.
* @param {object} threadsObj - An object where keys are thread IDs and values are message objects.
* @example
* const messages = {
* "thread-123": { "1678886400": { text: "Hello" } },
* "thread-456": { "1678886401": { text: "Hi there" } }
* };
* await privatri.storeMessages(messages);
*/
privatri.storeMessages = async (threadsObj = {}) => {
if (!_db) {
_log('error', 'Database not initialized. Please call privatri.init() first.');
return;
}
_log('log', 'Storing messages in IndexedDB...');
for (const [uuid, threadObj] of Object.entries(threadsObj)) {
// Ensure the object store exists before trying to write to it.
if (!_db.objectStoreNames.contains(uuid)) {
_log('warn', `Store "${uuid}" not found during message storage. You may need to re-init with this thread.`);
continue;
}
for (const [timestamp, messageObj] of Object.entries(threadObj)) {
await privatri.setValue(uuid, timestamp, messageObj);
}
}
_log('log', 'Finished storing messages.');
};
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
// Expose the component to the global window object
window.privatri = privatri;
const ctx = canvas.getContext("2d");
})(window);
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));
};
};
};
});

View File

@@ -0,0 +1,7 @@
<ul data-uuid="{{uuid}}" class="list hover:bg-base-200 transition">
<li class="list-row items-center">
<img class="size-12 rounded-full bg-base-content" src=""/>
<div class="text-xl font-medium">{{name}}</div>
<span class="badge h-8 bg-info text-info-content font-semibold border-transparent">17</span>
</li>
</ul>

View File

@@ -0,0 +1,88 @@
import apx from "./apx.js";
import { getOldestPrivatriids } from "./utils.js";
const bodyEl = document.querySelector("body");
const aliasListContainerEl = document.querySelector("#aliasListContainer");
let aliasesArray = [];
const newAlias = async (alias) => {
let aliasTemplate = "";
await fetch("./alias.mustache")
.then(res => res.text())
.then(template => {
aliasTemplate = template;
});
return Mustache.render(aliasTemplate, { alias });
};
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
(async () => {
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey;
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
aliasesArray = (await apx.indexedDB.get("privatri", "messages", privatriid)).aliases;
const ownerAlias = (await apx.indexedDB.get("privatri", "messages", privatriid)).owner;
for (const alias of aliasesArray) {
aliasListContainerEl.insertAdjacentHTML("beforeend", await newAlias({
decrypted: (await apx.crypto.decryptMessage(alias, privateKey)).data,
crypted: alias
}));
};
document.querySelectorAll("button.removeAliasBtn").forEach(btn => {
btn.addEventListener("click", async () => {
if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) {
if (btn.getAttribute("data-alias") !== ownerAlias) {
const alias = btn.getAttribute("data-alias");
aliasesArray = aliasesArray.filter(a => a !== alias);
btn.parentElement.parentElement.remove();
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
const messageObj = await apx.indexedDB.get("privatri", "messages", privatriid);
messageObj.aliases = aliasesArray;
await apx.indexedDB.set("privatri", "messages", messageObj);
};
});
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You cannot remove the owner of the thread."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to remove this alias."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
});
});
};
});
})();

View File

@@ -0,0 +1,6 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-100 h-full">
<h1 class="text-4xl flex-shrink-0 mt-8">Alias list</h1>
<div id="aliasListContainer" class="overflow-y-auto flex flex-col w-full flex-1 min-h-0">
</div>
</div>

View File

@@ -0,0 +1,72 @@
import apx from "./apx.js";
import { getOldestPrivatriids } from "./utils.js";
const bodyEl = document.querySelector("body");
const threadNameInputEl = document.querySelector("#threadNameInput");
const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn");
const applyModificationsBtnEl = document.querySelector("#applyModificationsBtn");
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
let messageObj = {};
(async () => {
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey;
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
let ownerAlias = "";
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
messageObj = await apx.indexedDB.get("privatri", "messages", privatriid);
ownerAlias = messageObj.owner;
threadNameInputEl.value = (await apx.crypto.decryptMessage(messageObj.title, privateKey)).data;
(Array.from(autoDeletionBtnElArray).find(el => el.getAttribute("data-auto-deletion") === String(messageObj.dt_autodestruction))).classList.add("bg-base-200");
if (messageObj.urgencydeletion === true) {
document.querySelector('input[type="checkbox"].toggle').checked = true;
};
};
});
applyModificationsBtnEl.addEventListener("click", async () => {
if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) {
messageObj.title = await apx.crypto.encryptMessage(threadNameInputEl.value, privateKey);
messageObj.dt_autodestruction = (() => {
const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200"));
return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10);
})();
messageObj.urgencydeletion = document.querySelector('input[type="checkbox"].toggle').checked;
await apx.indexedDB.set("privatri", "messages", messageObj);
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this thread."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
});
})();
autoDeletionBtnElArray.forEach(btn => {
btn.addEventListener("click", () => {
autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200"));
btn.classList.add("bg-base-200");
});
});

View File

@@ -0,0 +1,33 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit min-w-xs">
<h1 class="text-4xl">Thread settings</h1>
<div class="input rounded-lg w-full flex items-center gap-x-2 focus-within:outline focus-within:outline-2 focus-within:outline-primary bg-base-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="fill-current text-base-content">
<path d="M80 0v-160h800V0H80Zm160-320h56l312-311-29-29-28-28-311 312v56Zm-80 80v-170l448-447q11-11 25.5-17t30.5-6q16 0 31 6t27 18l55 56q12 11 17.5 26t5.5 31q0 15-5.5 29.5T777-687L330-240H160Zm560-504-56-56 56 56ZM608-631l-29-29-28-28 57 57Z"/>
</svg>
<input id="threadNameInput" type="text" placeholder="Thread name..." class="flex-1 bg-transparent border-none outline-none shadow-none"/>
</div>
<div class="dropdown dropdown-center w-full">
<div tabindex="0" role="button" class="btn w-full rounded-lg flex items-center justify-between">
Message auto destruction
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="fill-current text-base-content">
<path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box w-full z-1 w-52 p-2 shadow-sm">
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="86400000">
<a class="rounded-lg">Every days</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="604800000">
<a class="rounded-lg">Every weeks</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="2592000000">
<a class="rounded-lg">Every months</a>
</li>
</ul>
</div>
<div class="w-full flex items-center justify-left gap-x-4">
<input type="checkbox" class="toggle"/>
<p>Urgency deletion</p>
</div>
<button id="applyModificationsBtn" class="btn w-full rounded-lg mt-16">Apply modifications</button>
</div>

View File

@@ -0,0 +1,5 @@
<div class="toast">
<div class="alert alert-info">
<span>{{message}}</span>
</div>
</div>

46
wco/privatri/utils.js Normal file
View File

@@ -0,0 +1,46 @@
async function getOldestPrivatriids(dbName, storeName) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const cursorRequest = store.openCursor();
const uuidMap = {};
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const obj = cursor.value;
const [uuid, timestamp] = obj.privatriid.split("_");
if (!uuidMap[uuid] || Number(timestamp) < uuidMap[uuid].timestamp) {
uuidMap[uuid] = { privatriid: obj.privatriid, timestamp: Number(timestamp) };
};
cursor.continue();
} else {
const result = Object.values(uuidMap).map(event => event.privatriid);
resolve(result);
};
};
cursorRequest.onerror = (event) => reject(event);
};
request.onerror = (event) => reject(event);
});
};
async function syncronizeBackend(lastConnection) {
};
export {
getOldestPrivatriids,
syncronizeBackend
};