Compare commits

...

55 Commits

Author SHA1 Message Date
afe45ad960 Add optional argument promptName to
prepareAndSendPairingTx
2025-03-17 17:11:16 +01:00
ed0c11b527 Add dumpNeuteredDevice 2025-03-17 17:10:58 +01:00
0cfb02b06a [bug] empty strings doesn't count as missing data 2025-03-17 17:10:40 +01:00
f00faa1841 fix_bug_create_device 2025-03-17 17:10:17 +01:00
1952ade39c rolesContains{Us, Member} doesn't call wasm 2025-03-12 15:46:39 +01:00
e62a04ea57 Fix createDmProcess 2025-03-12 15:46:39 +01:00
b46a9aa13a Fix createProcessPairing 2025-03-12 15:46:39 +01:00
2aa846b8e5 [bug] don't add empty states to states we need to monitor 2025-03-12 15:46:39 +01:00
57bd945599 Make service worker skips empty states and public fields 2025-03-12 15:46:39 +01:00
c82c219a28 Update approveChange 2025-03-12 15:46:39 +01:00
3a851d1b62 fusionDev1Dev3 2025-03-12 10:56:26 +01:00
4ff2965b49 [WIP] Add an account creation modal 2025-03-07 16:44:11 +01:00
4d0c3e3f56 Add create accont Btn and update prepareAndSendPairingTx() 2025-03-07 16:42:28 +01:00
b9c4dfbfd9 Add fn to populate the member select form 2025-03-07 16:39:20 +01:00
cc0d823125 Update home page 2025-03-07 16:36:39 +01:00
090bdbedaa Various fix in chat 2025-03-04 14:55:57 +01:00
722e08ea82 Fix handleHandshakeMsg 2025-03-04 14:55:35 +01:00
3785285e4d decryptAttribute request missing data 2025-03-04 14:54:56 +01:00
3d09a20512 updateProcess takes a default empty public_data 2025-03-04 14:54:23 +01:00
de6213b1b7 requestDataFromPeers takes roles 2025-03-04 14:53:56 +01:00
dc231ba982 Don't post SCAN message when no process 2025-03-04 14:51:43 +01:00
570f98c51c Add roles to UserDiff when encountering missing data 2025-03-04 14:50:57 +01:00
b6c2d211e5 Update more aggressively processes on relay notifications 2025-03-03 23:24:41 +01:00
bde2ed0e44 Better handling of commitment errors 2025-03-03 23:24:15 +01:00
15626201a8 Replace descriptions with public_data 2025-03-03 23:23:59 +01:00
bb0d998c9f Add memberPublicName to pairing process 2025-03-03 23:23:38 +01:00
1747908d20 Take a username at pairing 2025-03-03 23:22:28 +01:00
7ad8063e87 Remove dead code 2025-03-03 21:27:30 +01:00
c32a7f9e86 Exclude pairing process from the chat list 2025-03-03 21:26:58 +01:00
7ebc5a75ea Redirect to chat after pairing 2025-03-03 21:26:25 +01:00
38b5567b68 Refactor loadAllMembers 2025-03-03 19:03:19 +01:00
e566c17a9a Add getPublicData 2025-03-03 19:02:43 +01:00
d243b58101 Refactor loadProcesses, display process name 2025-03-03 17:19:09 +01:00
6e57225529 Add getProcessName 2025-03-03 17:18:14 +01:00
7f6eabaa90 Remove unnecessary async for getRoles 2025-03-03 17:17:54 +01:00
8f34f2bcbe Change default page to chat 2025-03-03 16:36:19 +01:00
1bdb66b583 Properly compare members addresses instead of stringifying them 2025-03-03 10:53:13 +01:00
837ae35be9 Remove unnecessary check in lookForMyDms 2025-03-03 10:52:24 +01:00
4eb24d9678 [bug] forget to await sendMessage when pressing key 2025-03-03 10:51:47 +01:00
b4d35b5dc8 [bug] didn't set selectedChatProcessId when creating new dm 2025-03-03 10:50:35 +01:00
9f05099338 Remove dead code 2025-02-28 18:33:09 +01:00
f2991dd130 Remove some unused variables 2025-02-28 18:26:19 +01:00
c8d049ce85 getAllMembers and getAllMembersSorted 2025-02-28 18:26:09 +01:00
5bda1736e1 Don't send process-updated event for process unchanged 2025-02-28 18:25:43 +01:00
db07abaa8a Fixed process highlighting + minor improvements 2025-02-28 18:24:51 +01:00
a1aec7cec3 Fix process screen 2025-02-28 16:44:23 +01:00
7c3e263b8a Chat updates when received message 2025-02-27 12:34:56 +01:00
ed578be468 Add isLoading variable 2025-02-26 09:33:07 +01:00
d950ce0a2b refactor service 2025-02-26 09:32:00 +01:00
b7a2f3a058 Refactor database.service 2025-02-26 09:29:36 +01:00
aa22b78121 Refactor worker 2025-02-26 09:28:30 +01:00
85a2296d56 Minor fixes 2025-02-24 11:06:07 +01:00
6180a66405 Refresh Member list when create a Dm Process 2025-02-24 11:05:45 +01:00
8ce490eb7b Update lookForMyDms 2025-02-24 11:03:30 +01:00
0fe9a6f371 [bug] sorted members to the top of the list 2025-02-21 11:52:48 +01:00
15 changed files with 955 additions and 734 deletions

View File

@ -5,7 +5,7 @@
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client/pkg ../sdk_client --target bundler --dev",
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
"start": "vite --host 0.0.0.0",
"build": "tsc && vite build",
"deploy": "sudo cp -r dist/* /var/www/html/",

View File

@ -599,7 +599,7 @@ body {
margin-top: 9vh;
margin-left: -1%;
text-align: left;
width: 209vh;
width: 100vw;
}
/* Liste des information sur l'account */
@ -1364,3 +1364,62 @@ body {
border-radius: 50%;
border: 2px solid white;
}
/* ---------------------Style pour le QR code--------------------- */
.qr-code {
width: 50px;
height: 50px;
cursor: pointer;
transition: transform 0.2s ease;
}
.qr-code:hover {
transform: scale(1.5);
}
.qr-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.qr-modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
position: relative;
text-align: center;
}
.close-qr-modal {
position: absolute;
right: 10px;
top: 5px;
font-size: 24px;
cursor: pointer;
color: #666;
}
.close-qr-modal:hover {
color: #000;
}
.qr-code-large {
max-width: 300px;
margin: 10px 0;
}
.qr-address {
margin-top: 10px;
word-break: break-all;
font-size: 12px;
color: #666;
}

View File

@ -316,7 +316,7 @@ h1 {
margin-top: 0px;
}
.sp-address-btn {
.create-btn {
margin-bottom: 2em;
cursor: pointer;
background-color: #d0d0d7;
@ -778,3 +778,41 @@ select[data-multi-select-plugin] {
.process-card-action {
width: 100%;
}
/**************************************** Select Member Home Page ******************************************************/
.custom-select {
width: 100%;
max-height: 150px;
overflow-y: auto;
direction: ltr;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
}
.custom-select option {
padding: 8px 12px;
cursor: pointer;
}
.custom-select option:hover {
background-color: #f0f0f0;
}
.custom-select::-webkit-scrollbar {
width: 8px;
}
.custom-select::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-select::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.custom-select::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@ -0,0 +1,14 @@
<div id="creation-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Do you want to create a 4NK member?<br />
Attempting to create a member with address <br />
<strong>{{device1}}</strong> <br />
</div>
<div class="confirmation-box">
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@ declare global {
interface Window {
initAccount: () => void;
showContractPopup: (contractId: string) => void;
showPairing: () => void;
showPairing: () => Promise<void>;
showWallet: () => void;
showData: () => void;
addWalletRow: () => void;
@ -36,6 +36,7 @@ declare global {
generateRecoveryWords: () => string[];
exportUserData: () => void;
updateActionButtons: () => void;
showQRCodeModal: (address: string) => void;
}
}
@ -45,6 +46,7 @@ import { Row, WalletRow, DataRow, Notification, Contract, NotificationMessage }
import { addressToEmoji } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/document.utils';
import accountStyle from '../../../public/style/account.css?inline';
import Services from '../../services/service';
let isAddingRow = false;
let currentRow: HTMLTableRowElement | null = null;
@ -197,6 +199,7 @@ class AccountElement extends HTMLElement {
window.updateActionButtons = () => this.updateActionButtons();
window.openAvatarPopup = () => this.openAvatarPopup();
window.closeAvatarPopup = () => this.closeAvatarPopup();
window.showQRCodeModal = (address: string) => this.showQRCodeModal(address);
if (!localStorage.getItem('rows')) {
localStorage.setItem('rows', JSON.stringify(defaultRows));
@ -552,6 +555,13 @@ private updateTableContent(rows: Row[]): void {
<td>${row.column1}</td>
<td class="device-name" onclick="window.editDeviceName(this)">${row.column2}</td>
<td>${row.column3}</td>
<td>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=50x50&data=${encodeURIComponent(row.column1)}"
alt="QR Code"
title="${row.column1}"
class="qr-code"
onclick="window.showQRCodeModal('${encodeURIComponent(row.column1)}')">
</td>
<td>
<button class="delete-button" onclick="window.deleteRow(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill="red">
@ -627,29 +637,26 @@ private deleteRow(button: HTMLButtonElement): void {
const table = row.closest('tbody');
if (!table) return;
// Vérifier le nombre de lignes restantes
const remainingRows = table.getElementsByTagName('tr').length;
if (remainingRows <= 2) {
this.showAlert('You must keep at least 2 devices paired');
return;
}
// Animation de suppression
row.style.transition = 'opacity 0.3s';
const index = Array.from(table.children).indexOf(row);
row.style.transition = 'opacity 0.3s, transform 0.3s';
row.style.opacity = '0';
row.style.transform = 'translateX(-100%)';
setTimeout(() => {
// Obtenir l'index avant la suppression
const index = Array.from(table.children).indexOf(row);
// Supprimer la ligne du DOM
row.remove();
// Mettre à jour le localStorage
const storageKey = STORAGE_KEYS[currentMode];
const rows = JSON.parse(localStorage.getItem(storageKey) || '[]');
rows.splice(index, 1);
localStorage.setItem(storageKey, JSON.stringify(rows));
if (index > -1) {
rows.splice(index, 1);
localStorage.setItem(storageKey, JSON.stringify(rows));
}
}, 300);
}
@ -887,9 +894,6 @@ private showContractPopup(contractId: string) {
});
}
// Ajouter à l'objet window
// Fonction utilitaire pour cacher tous les contenus
private hideAllContent(): void {
const contents = ['pairing-content', 'wallet-content', 'process-content', 'data-content'];
@ -902,29 +906,26 @@ private hideAllContent(): void {
}
// Fonctions d'affichage des sections
private showPairing(): void {
private async showPairing(): Promise<void> {
const service = await Services.getInstance();
const spAddress = await service.getDeviceAddress();
isAddingRow = false;
currentRow = null;
currentMode = 'pairing';
// Cacher tous les contenus
this.hideAllContent();
// Mettre à jour le titre
const headerElement = this.shadowRoot?.getElementById('parameter-header');
if (headerElement) {
headerElement.textContent = 'Pairing';
}
// Afficher le contenu de pairing
const pairingContent = this.shadowRoot?.getElementById('pairing-content');
if (pairingContent) {
pairingContent.style.display = 'block';
pairingContent.innerHTML = `
<div class="parameter-header" id="parameter-header">Pairing</div>
<div class="parameter-header" id="parameter-header">Pairing</div>
<div class="table-container">
<table class="parameter-table" id="pairing-table">
<thead>
@ -932,7 +933,8 @@ private showPairing(): void {
<th>SP Address</th>
<th>Device Name</th>
<th>SP Emojis</th>
<th></th>
<th>QR Code</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
@ -942,9 +944,47 @@ private showPairing(): void {
</div>
</div>
`;
let rows = JSON.parse(localStorage.getItem(STORAGE_KEYS.pairing) || '[]');
const deviceExists = rows.some((row: Row) => row.column1 === spAddress);
if (!deviceExists && spAddress) {
const emojis = await addressToEmoji(spAddress);
try {
// Déboguer le processus de pairing
const pairingProcessId = await service.getPairingProcessId();
console.log('Pairing Process ID:', pairingProcessId);
const pairingProcess = await service.getProcess(pairingProcessId);
console.log('Pairing Process:', pairingProcess);
const userName = pairingProcess?.states?.[0]?.metadata?.userName
|| pairingProcess?.states?.[0]?.metadata?.name
|| localStorage.getItem('userName')
console.log('Username found:', userName);
const newRow = {
column1: spAddress,
column2: userName,
column3: emojis
};
rows = [newRow, ...rows];
localStorage.setItem(STORAGE_KEYS.pairing, JSON.stringify(rows));
} catch (error) {
console.error('Error getting pairing process:', error);
const newRow = {
column1: spAddress,
column2: 'This Device',
column3: emojis
};
rows = [newRow, ...rows];
localStorage.setItem(STORAGE_KEYS.pairing, JSON.stringify(rows));
}
}
// Mettre à jour le contenu du tableau
const rows = JSON.parse(localStorage.getItem(STORAGE_KEYS.pairing) || '[]');
this.updateTableContent(rows);
}
}
@ -1419,6 +1459,28 @@ private initializeEventListeners() {
avatarInput.addEventListener('change', this.handleAvatarUpload.bind(this));
}
}
private showQRCodeModal(address: string): void {
const modal = document.createElement('div');
modal.className = 'qr-modal';
modal.innerHTML = `
<div class="qr-modal-content">
<span class="close-qr-modal">&times;</span>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${address}"
alt="QR Code Large"
class="qr-code-large">
<div class="qr-address">${decodeURIComponent(address)}</div>
</div>
`;
this.shadowRoot?.appendChild(modal);
const closeBtn = modal.querySelector('.close-qr-modal');
closeBtn?.addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
}
}
customElements.define('account-element', AccountElement);

View File

@ -13,6 +13,7 @@ import Database from '../../services/database.service';
import Services from '../../services/service';
const storageUrl = `/storage`;
const defaultProcessName = 'Unnamed Process';
interface LocalNotification {
memberId: string;
@ -34,7 +35,7 @@ class ChatElement extends HTMLElement {
return ['process-id'];
}
private processId: string | null = null;
private selectedChatProcessId: string | null = null;
private processRoles: any | null = null;
private selectedMember: string | null = null;
private notifications: LocalNotification[] = [];
@ -48,9 +49,10 @@ class ChatElement extends HTMLElement {
}));
private messageState: number = 0;
private selectedRole: string | null = null;
private userProcessSet: Set<string> = new Set();
private dmMembersSet: Set<string> = new Set();
private addressMap: Record<string, string> = {};
private addressMap: Record<string, string> = {};
private isLoading = false;
constructor() {
super();
@ -121,6 +123,23 @@ class ChatElement extends HTMLElement {
}
};
document.addEventListener('newDataReceived', async (event: CustomEvent) => {
const { detail } = event;
console.log('New data event received:', JSON.stringify(detail));
if (detail.processId && detail.processId === this.selectedChatProcessId) {
console.log('Detected update to chat');
if (this.selectedMember) {
await this.loadMemberChat(this.selectedMember);
} else {
console.error('No selected member?');
}
} else {
console.log('Received an update for another process');
}
});
document.addEventListener('DOMContentLoaded', () => {
this.notificationBadge = document.querySelector('.notification-badge');
this.notificationBoard = document.getElementById('notification-board');
@ -131,7 +150,6 @@ class ChatElement extends HTMLElement {
}
});
// Initialiser les événements de notification
document.addEventListener('click', (event: Event): void => {
if (this.notificationBoard && this.notificationBoard.style.display === 'block' &&
@ -144,19 +162,11 @@ class ChatElement extends HTMLElement {
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log(`🔄 Attribute ${name} changed from ${oldValue} to ${newValue}`);
if (name === 'process-id' && newValue) {
console.log('🔍 Loading chat with new process ID:', newValue);
this.loadGroupListFromAProcess(newValue);
}
}
private initMessageEvents() {
const sendButton = this.shadowRoot?.querySelector('#send-button');
if (sendButton) {
sendButton.addEventListener('click', () => {
this.sendMessage();
sendButton.addEventListener('click', async () => {
await this.sendMessage();
setTimeout(async () => await this.reloadMemberChat(this.selectedMember), 600);
messageInput.value = '';
});
@ -164,11 +174,11 @@ class ChatElement extends HTMLElement {
const messageInput = this.shadowRoot?.querySelector('#message-input');
if (messageInput) {
messageInput.addEventListener('keypress', (event: Event) => {
messageInput.addEventListener('keypress', async (event: Event) => {
const keyEvent = event as KeyboardEvent;
if (keyEvent.key === 'Enter' && !keyEvent.shiftKey) {
event.preventDefault();
this.sendMessage();
await this.sendMessage();
setTimeout(async () => await this.reloadMemberChat(this.selectedMember), 600);
messageInput.value = '';
}
@ -273,7 +283,7 @@ class ChatElement extends HTMLElement {
return;
}
if (!this.processId) {
if (!this.selectedChatProcessId) {
console.error('no process id set');
return;
}
@ -306,8 +316,8 @@ class ChatElement extends HTMLElement {
}
};
console.log("----this.processId",this.processId );
const process = await service.getProcess(this.processId);
console.log("----this.selectedChatProcessId",this.selectedChatProcessId );
const process = await service.getProcess(this.selectedChatProcessId);
if (!process) {
console.error('Failed to retrieve process from DB');
@ -335,14 +345,26 @@ class ChatElement extends HTMLElement {
console.log(`newStateId: ${newStateId}`);
await service.handleApiReturn(apiReturn);
const createPrdReturn = service.createPrdUpdate(this.processId, newStateId);
const createPrdReturn = service.createPrdUpdate(this.selectedChatProcessId, newStateId);
await service.handleApiReturn(createPrdReturn);
// Now we validate the new state
const approveChangeReturn = service.approveChange(this.processId, newStateId);
const approveChangeReturn = await service.approveChange(this.selectedChatProcessId, newStateId);
await service.handleApiReturn(approveChangeReturn);
await this.loadMemberChat(this.selectedMember);
await this.lookForMyDms();
const groupList = this.shadowRoot?.querySelector('#group-list');
const tabs = this.shadowRoot?.querySelectorAll('.tab');
const memberList = groupList?.querySelector('.member-list');
if (memberList) {
memberList.innerHTML = '';
await this.loadAllMembers();
if (tabs) {
await this.switchTab('members', tabs);
}
}
} catch (error) {
console.error('❌ Error in sendMessage:', error);
}
@ -366,16 +388,16 @@ class ChatElement extends HTMLElement {
private async lookForChildren(): Promise<string | null> {
// Filter processes for the children of current process
const service = await Services.getInstance();
if (!this.processId) {
if (!this.selectedChatProcessId) {
console.error('No process id');
return null;
}
const children: string[] = await service.getChildrenOfProcess(this.processId);
const children: string[] = await service.getChildrenOfProcess(this.selectedChatProcessId);
const processRoles = this.processRoles;
const selectedMember = this.selectedMember;
for (const child of children) {
const roles = await service.getRoles(JSON.parse(child));
const roles = service.getRoles(JSON.parse(child));
// Check that we and the other members are in the role
if (!service.isChildRole(processRoles, roles)) {
console.error('Child process roles are not a subset of parent')
@ -401,106 +423,123 @@ class ChatElement extends HTMLElement {
const service = await Services.getInstance();
const members = await service.getAllMembers();
const database = await Database.getInstance();
const db = database.db;
const processes = await service.getProcesses();
const memberList = document.createElement('ul');
memberList.className = 'member-list active';
const prioritizedMembers: [string, any][] = [];
const remainingMembers: [string, any][] = [];
// Partition members into prioritized and remaining arrays.
const prioritizedMembers: [string, Member][] = [];
const remainingMembers: [string, Member][] = [];
for (const [processId, member] of Object.entries(members)) {
if (this.dmMembersSet.has(processId)) {
prioritizedMembers.push([processId, member]);
prioritizedMembers.push([processId, member]);
} else {
remainingMembers.push([processId, member]);
remainingMembers.push([processId, member]);
}
}
const sortedMembers = prioritizedMembers.concat(remainingMembers);
for (const [processId, member] of Object.entries(members)) {
// Process each member.
for (const [processId, member] of sortedMembers) {
const memberItem = document.createElement('li');
memberItem.className = 'member-item';
// Apply special styling if the member is prioritized.
if (this.dmMembersSet.has(processId)) {
memberItem.style.cssText = `
background-color: var(--accent-color);
transition: background-color 0.3s ease;
cursor: pointer;
`;
memberItem.onmouseover = () => {
memberItem.style.backgroundColor = 'var(--accent-color-hover)';
};
memberItem.onmouseout = () => {
memberItem.style.backgroundColor = 'var(--accent-color)';
};
memberItem.style.cssText = `
background-color: var(--accent-color);
transition: background-color 0.3s ease;
cursor: pointer;
`;
memberItem.addEventListener('mouseover', () => {
memberItem.style.backgroundColor = 'var(--accent-color-hover)';
});
memberItem.addEventListener('mouseout', () => {
memberItem.style.backgroundColor = 'var(--accent-color)';
});
}
// Create a container for the member content.
const memberContainer = document.createElement('div');
memberContainer.className = 'member-container';
// Create the emoji span and load its label.
const emojiSpan = document.createElement('span');
emojiSpan.className = 'member-emoji';
const emojis = await addressToEmoji(processId);
emojiSpan.dataset.emojis = emojis;
const transaction = db.transaction("labels", "readonly");
const store = transaction.objectStore("labels");
const request = store.get(emojis);
request.onsuccess = () => {
const label = request.result;
emojiSpan.textContent = label ? `${label.label} (${emojis})` : `Member (${emojis})`;
};
request.onerror = () => {
emojiSpan.textContent = `Member (${emojis})`;
};
// Get the member name, if any, and add it to the display
const process = processes[processId];
let memberPublicName;
if (process) {
const publicMemberData = service.getPublicData(process);
if (publicMemberData) {
const extractedName = publicMemberData['memberPublicName'];
if (extractedName !== undefined && extractedName !== null) {
memberPublicName = extractedName;
}
}
}
if (!memberPublicName) {
memberPublicName = 'Unnamed Member';
}
emojiSpan.textContent = `${memberPublicName} (${emojis})`
memberContainer.appendChild(emojiSpan);
memberItem.appendChild(memberContainer);
// Add click handler to load member chat.
memberItem.addEventListener('click', async () => {
await this.loadMemberChat(processId);
await this.loadMemberChat(processId);
});
// Create and configure the edit label button.
const editLabelButton = document.createElement('button');
editLabelButton.className = 'edit-label-button';
editLabelButton.textContent = "✏️";
editLabelButton.addEventListener("click", (event) => {
event.stopPropagation();
});
editLabelButton.addEventListener("dblclick", async (event) => {
event.stopPropagation();
event.preventDefault();
const newLabel = prompt("Set a new name for the member:");
if (!newLabel) return;
const editTransaction = db.transaction("labels", "readwrite");
const editStore = editTransaction.objectStore("labels");
const labelObject = { emoji: emojis, label: newLabel };
const putRequest = editStore.put(labelObject);
putRequest.onsuccess = () => {
emojiSpan.textContent = `${newLabel} : ${emojis}`;
this.reloadMemberChat(processId);
};
const db = await Database.getInstance();
this.updateLabelForEmoji(emojis, newLabel, db, emojiSpan, processId);
});
memberList.appendChild(memberItem);
memberContainer.appendChild(editLabelButton);
memberList.appendChild(memberItem);
}
groupList.appendChild(memberList);
}
// Helper function to update a label in IndexedDB.
private updateLabelForEmoji(
emojis: string,
newLabel: string,
db: IDBDatabase,
emojiSpan: HTMLElement,
processId: string
) {
const transaction = db.transaction("labels", "readwrite");
const store = transaction.objectStore("labels");
const labelObject = { emoji: emojis, label: newLabel };
const request = store.put(labelObject);
request.onsuccess = () => {
emojiSpan.textContent = `${newLabel} : ${emojis}`;
this.reloadMemberChat(processId);
};
}
private async lookForDmProcess(): Promise<string | null> {
const service = await Services.getInstance();
const processes = await service.getMyProcesses();
@ -513,12 +552,12 @@ class ChatElement extends HTMLElement {
const process = await service.getProcess(processId);
console.log(process);
const state = process.states[0]; // We assume that description never change and that we are part of the process from the beginning
const description = await service.decryptAttribute(state, 'description');
const description = await service.decryptAttribute(processId, state, 'description');
console.log(description);
if (!description || description !== "dm") {
continue;
}
const roles = await service.getRoles(process);
const roles = service.getRoles(process);
if (!service.rolesContainsMember(roles, recipientAddresses)) {
console.error('Member is not part of the process');
continue;
@ -544,40 +583,38 @@ class ChatElement extends HTMLElement {
for (const processId of processes) {
const process = await service.getProcess(processId);
const state = process.states[0];
const description = await service.decryptAttribute(state, 'description');
const description = await service.decryptAttribute(processId, state, 'description');
if (!description || description !== "dm") {
continue;
}
const roles = await service.getRoles(process);
if (!service.rolesContainsMember(roles, myAddresses)) {
continue;
}
const roles = service.getRoles(process);
const members = roles.dm.members;
for (const member of members) {;
if (JSON.stringify(member.sp_addresses) !== JSON.stringify(myAddresses)) {
this.dmMembersSet.add(member.sp_addresses);
}
if (!service.compareMembers(member.sp_addresses, myAddresses)) {
for (const [id, mem] of Object.entries(allMembers)) {
if (service.compareMembers(mem.sp_addresses, member.sp_addresses)) {
this.dmMembersSet.add(id);
break;
}
}
}
}
}
const updatedDmMembersSet = new Set<string>();
for (const dmMember of this.dmMembersSet) {
for (const [processId, member] of Object.entries(allMembers)) {
if (JSON.stringify(member.sp_addresses) === JSON.stringify(dmMember)) {
updatedDmMembersSet.add(processId);
}
}
}
this.dmMembersSet = updatedDmMembersSet;
} catch (e) {
console.error(e);
}
console.log("SET DE MEMBRES AVEC QUI JE DM:", this.dmMembersSet);
console.log("dmMembersSet:", this.dmMembersSet);
return null;
}
private async loadMemberChat(pairingProcess: string) {
if (this.isLoading) {
console.log('Already loading messages, skipping...');
return;
}
try {
this.isLoading = true;
const service = await Services.getInstance();
const myAddresses = await service.getMemberFromDevice();
const database = await Database.getInstance();
@ -596,6 +633,8 @@ class ChatElement extends HTMLElement {
const messagesContainer = this.shadowRoot?.querySelector('#messages');
if (!chatHeader || !messagesContainer) return;
messagesContainer.innerHTML = '';
const emojis = await addressToEmoji(pairingProcess);
@ -612,8 +651,6 @@ class ChatElement extends HTMLElement {
chatHeader.textContent = `Chat with member (${emojis})`;
};
messagesContainer.innerHTML = '';
let dmProcessId = await this.lookForDmProcess();
if (dmProcessId === null) {
@ -632,12 +669,10 @@ class ChatElement extends HTMLElement {
setTimeout(async () => {
// Now create a first commitment
console.log('Created a dm process', processId);
this.processId = processId;
this.selectedChatProcessId = processId;
const createPrdReturn = await service.createPrdUpdate(processId, stateId);
console.log(createPrdReturn);
await service.handleApiReturn(createPrdReturn);
const approveChangeReturn = service.approveChange(processId, stateId);
console.log(approveChangeReturn);
const approveChangeReturn = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveChangeReturn);
}, 500);
} catch (e) {
@ -651,7 +686,7 @@ class ChatElement extends HTMLElement {
}
} else {
console.log('Found DM process', dmProcessId);
this.processId = dmProcessId;
this.selectedChatProcessId = dmProcessId;
}
/* TODO
@ -669,25 +704,25 @@ class ChatElement extends HTMLElement {
// Récupérer les messages depuis les états du processus
const allMessages: any[] = [];
const dmProcess = await service.getProcess(dmProcessId);
const dmProcess = await service.getProcess(this.selectedChatProcessId);
console.log(dmProcess);
if (dmProcess?.states) {
for (const state of dmProcess.states) {
const pcd_commitment = state.pcd_commitment;
const message = await service.decryptAttribute(state, 'message');
if (state.state_id === '') { continue; }
const message = await service.decryptAttribute(this.selectedChatProcessId, state, 'message');
if (message === "" || message === undefined || message === null) {
continue;
}
console.log('message', message);
allMessages.push(message);
}
continue;
}
console.log('message', message);
allMessages.push(message);
}
}
allMessages.sort((a, b) => a.metadata.createdAt - b.metadata.createdAt);
if (allMessages.length > 0) {
console.log('Messages found:', allMessages);
allMessages.sort((a, b) => a.metadata.createdAt - b.metadata.createdAt);
for (const message of allMessages) {
const messageElement = document.createElement('div');
messageElement.className = 'message-container';
@ -755,6 +790,8 @@ class ChatElement extends HTMLElement {
this.scrollToBottom(messagesContainer);
} catch (error) {
console.error('❌ Error in loadMemberChat:', error);
} finally {
this.isLoading = false;
}
}
@ -768,6 +805,8 @@ class ChatElement extends HTMLElement {
const messagesContainer = this.shadowRoot?.querySelector('#messages');
if (!chatHeader || !messagesContainer) return;
messagesContainer.innerHTML = '';
const emojis = await addressToEmoji(pairingProcess);
@ -777,16 +816,17 @@ class ChatElement extends HTMLElement {
request.onsuccess = () => {
const label = request.result;
chatHeader.textContent = label ? `Chat with ${label.label} (${emojis})` : `Chat with member (${emojis})`;
if (this.selectedMember === pairingProcess) {
chatHeader.textContent = label ? `Chat with ${label.label} (${emojis})` : `Chat with member (${emojis})`;
}
};
request.onerror = () => {
chatHeader.textContent = `Chat with member (${emojis})`;
};
messagesContainer.innerHTML = '';
let dmProcessId = await this.processId;
let dmProcessId = await this.selectedChatProcessId;
// Récupérer les messages depuis les états du processus
const allMessages: any[] = [];
@ -797,8 +837,8 @@ class ChatElement extends HTMLElement {
if (dmProcess?.states) {
for (const state of dmProcess.states) {
const pcd_commitment = state.pcd_commitment;
const message = await service.decryptAttribute(state, 'message');
if (!state.state_id) { continue; }
const message = await service.decryptAttribute(dmProcessId, state, 'message');
if (message === "" || message === undefined || message === null) {
continue;
}
@ -1007,6 +1047,8 @@ class ChatElement extends HTMLElement {
private async switchTab(tabType: string, tabs: NodeListOf<Element>) {
const service = await Services.getInstance();
// Mettre à jour les classes des onglets
tabs.forEach(tab => {
tab.classList.toggle('active', tab.getAttribute('data-tab') === tabType);
@ -1026,7 +1068,7 @@ class ChatElement extends HTMLElement {
// Charger le contenu approprié
switch (tabType) {
case 'processes':
const processSet = await this.getProcessesWhereTheCurrentMemberIs();
const processSet = await service.getMyProcesses();
await this.loadAllProcesses(processSet);
break;
case 'members':
@ -1039,13 +1081,14 @@ class ChatElement extends HTMLElement {
}
//load all processes from the service
private async loadAllProcesses(processSet: Set<string>) {
private async loadAllProcesses() {
console.log('🎯 Loading all processes');
this.closeSignature();
const allProcesses = await this.getProcesses();
// Afficher les processus dans le container #group-list
const service = await Services.getInstance();
const allProcesses: Record<string, Process> = await service.getProcesses();
const myProcesses: string[] = await service.getMyProcesses();
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) {
console.warn('⚠️ Group list element not found');
@ -1074,94 +1117,102 @@ class ChatElement extends HTMLElement {
});
//trier les processus : ceux de l'utilisateur en premier
allProcesses.sort((a, b) => {
const aInSet = this.userProcessSet.has(a.value.states[0].commited_in);
const bInSet = this.userProcessSet.has(b.value.states[0].commited_in);
return bInSet ? 1 : aInSet ? -1 : 0;
});
const sortedEntries = Object.entries(allProcesses).sort(
([keyA], [keyB]) => {
const inSetA = myProcesses.includes(keyA);
const inSetB = myProcesses.includes(keyB);
return inSetB ? 1 : inSetA ? -1 : 0;
}
);
for (const process of allProcesses) {
for (const [processId, process] of sortedEntries) {
// Create and configure the main list item.
const li = document.createElement('li');
li.className = 'group-list-item';
const oneProcess = process.value.states[0].commited_in;
let roles;
try {
//roles = await service.getRoles(process);
if (!roles) {
roles = await process.value.states[0]?.roles;
}
} catch (e) {
// console.error('Failed to get roles for process:', process);
li.setAttribute('data-process-id', processId);
// Retrieve roles for the current process.
const roles = service.getRoles(process);
if (!roles) {
console.error('Failed to get roles for process:', process);
continue;
}
// Si le processus est dans notre Set, ajouter la classe my-process
if (this.userProcessSet && this.userProcessSet.has(oneProcess)) {
li.style.cssText = `
background-color: var(--accent-color);
transition: background-color 0.3s ease;
cursor: pointer;
`;
li.onmouseover = () => {
li.style.backgroundColor = 'var(--accent-color-hover)';
};
li.onmouseout = () => {
li.style.backgroundColor = 'var(--accent-color)';
};
console.log("✅ Processus trouvé dans le set:", oneProcess);
// If process is a pairing process, we don't want it in the list
if (service.isPairingProcess(roles)) {
continue;
}
li.setAttribute('data-process-id', oneProcess);
//----MANAGE THE CLICK ON PROCESS ----
li.onclick = async (event) => {
const publicData = service.getPublicData(process);
const processName = publicData['processName'];
const emoji = await addressToEmoji(processId);
let displayName;
if (processName) {
displayName = `${processName} (${emoji})`;
} else {
displayName = `${defaultProcessName} (${emoji})`;
}
// If the process is part of myProcesses, apply special styling.
if (myProcesses && myProcesses.includes(processId)) {
li.style.cssText = `
background-color: var(--accent-color);
transition: background-color 0.3s ease;
cursor: pointer;
`;
li.addEventListener('mouseover', () => {
li.style.backgroundColor = 'var(--accent-color-hover)';
});
li.addEventListener('mouseout', () => {
li.style.backgroundColor = 'var(--accent-color)';
});
console.log("✅ Processus trouvé dans le set:", processId);
}
// Attach a click handler for the process.
li.addEventListener('click', async (event) => {
event.stopPropagation();
console.log("CLICKED ON PROCESS:", oneProcess);
//viser le h1 de signature-header
console.log("CLICKED ON PROCESS:", processId);
// Update the signature header with the corresponding emoji.
const signatureHeader = this.shadowRoot?.querySelector('.signature-header h1');
if (signatureHeader) {
const emoji = await addressToEmoji(oneProcess);
signatureHeader.textContent = `Signature of ${emoji}`;
if (processName) {
signatureHeader.textContent = `Signature of ${displayName}`;
} else {
signatureHeader.textContent = `Signature of ${displayName}`;
}
}
this.openSignature();
//afficher les roles dans chaque processus
this.openSignature();
console.log('🎯 Roles de signature:', roles);
await this.loadAllRolesAndMembersInSignature(roles);
//----MANAGE THE CLICK ON NEW REQUEST ----
await this.newRequest(oneProcess);
};
groupList.appendChild(li);
const container = document.createElement('div');
container.className = 'group-item-container';
const nameSpan = document.createElement('span');
nameSpan.textContent = `Process : `;
nameSpan.className = 'process-name';
container.appendChild(nameSpan);
addressToEmoji(oneProcess).then(emojis => {
const emojiSpan = document.createElement('span');
emojiSpan.className = 'process-emoji';
emojiSpan.textContent = emojis;
container.appendChild(emojiSpan);
await this.newRequest(processId);
});
// Create the container for the process name and emoji.
const container = document.createElement('div');
container.className = 'group-item-container';
// Create and set the process name element.
const nameSpan = document.createElement('span');
nameSpan.className = 'process-name';
nameSpan.textContent = displayName;
container.appendChild(nameSpan);
li.appendChild(container);
// afficher les roles dans chaque processus
//console.log('🎯 Roles:', roles);
// Create a hidden list for roles.
const roleList = document.createElement('ul');
roleList.className = 'role-list';
(roleList as HTMLElement).style.display = 'none';
roleList.style.display = 'none';
// Traiter chaque rôle
Object.entries(roles).forEach(([roleName, roleData]: [string, any]) => {
// Process each role and create role items.
Object.entries(roles).forEach(([roleName, roleData]) => {
const roleItem = document.createElement('li');
roleItem.className = 'role-item';
const roleContainer = document.createElement('div');
roleContainer.className = 'role-item-container';
@ -1169,41 +1220,46 @@ class ChatElement extends HTMLElement {
roleNameSpan.className = 'role-name';
roleNameSpan.textContent = roleName;
// Filtrer les membres dupliqués ici, avant de les passer à toggleMembers
const uniqueMembers = new Map<string, any>();
roleData.members?.forEach((member: any) => {
const spAddress = member.sp_addresses?.[0];
if (spAddress && !uniqueMembers.has(spAddress)) {
uniqueMembers.set(spAddress, member);
}
// Filter duplicate members by using the first sp_address as a key.
const uniqueMembers = new Map();
roleData.members?.forEach(member => {
const spAddress = member.sp_addresses?.[0];
if (spAddress && !uniqueMembers.has(spAddress)) {
uniqueMembers.set(spAddress, member);
}
});
// Créer un nouveau roleData avec les membres uniques
// Create a new roleData object with unique members.
const filteredRoleData = {
...roleData,
members: Array.from(uniqueMembers.values())
members: Array.from(uniqueMembers.values()),
};
// Attach a click handler for the role.
roleContainer.addEventListener('click', async (event) => {
console.log("CLICKED ON ROLE:", roleName);
event.stopPropagation();
await this.toggleMembers(filteredRoleData, roleItem);
event.stopPropagation();
console.log("CLICKED ON ROLE:", roleName);
await this.toggleMembers(filteredRoleData, roleItem);
});
roleContainer.appendChild(roleNameSpan);
roleItem.appendChild(roleContainer);
roleList.appendChild(roleItem);
});
li.appendChild(roleList);
groupList.appendChild(li);
// Toggle role list display when the container is clicked.
container.addEventListener('click', (event) => {
event.stopPropagation();
container.classList.toggle('expanded');
roleList.style.display = container.classList.contains('expanded') ? 'block' : 'none';
});
}
// Append the completed process list item once.
groupList.appendChild(li);
}
}
private async newRequest(processId: string) {
@ -1347,7 +1403,7 @@ class ChatElement extends HTMLElement {
console.log("Process récupéré:", process);
// Récupérer les rôles directement depuis le dernier état
const roles = await service.getRoles(process);
const roles = service.getRoles(process);
console.log("Roles trouvés:", roles);
if (!roles) return [];
@ -1426,7 +1482,6 @@ class ChatElement extends HTMLElement {
}
}
//Load tous les processus où le sp_adress est impliqué et renvoie un tableau d'adresses de processus
private async getMyProcessId() {
const service = await Services.getInstance();
return service.getPairingProcessId();
@ -1485,126 +1540,6 @@ class ChatElement extends HTMLElement {
}
}
// Load the group list from all processes
public async loadAllGroupListFromMyProcess(): Promise<void> {
console.log('🎯 Loading all group list');
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) {
console.error('❌ Group list element not found');
return;
}
const processes = await this.getProcesses();
if (!processes || Object.keys(processes).length === 0) {
console.log('⚠️ No processes found');
return;
}
for (const {key} of processes) {
const processName = await key;
console.log("Je suis l'id process de la boucle :" ,processName);
this.loadGroupListFromAProcess(processName);
}
}
// Load the group list from a process
private async loadGroupListFromAProcess(processId: string): Promise<void> {
console.log('Loading group list with processId:', processId);
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) return;
groupList.innerHTML = '';
this.processId = processId;
const service = await Services.getInstance();
const process = await service.getProcess(this.processId);
const roles = await service.getRoles(process);
if (roles === null) {
console.error('no roles in process');
return;
}
this.processRoles = roles;
console.log('🔑 Roles found:', this.processRoles);
const li = document.createElement('li');
li.className = 'group-list-item';
li.setAttribute('data-process-id', processId);
const container = document.createElement('div');
container.className = 'group-item-container';
const nameSpan = document.createElement('span');
nameSpan.textContent = `Process : `;
nameSpan.className = 'process-name';
container.appendChild(nameSpan);
await addressToEmoji(processId).then(emojis => {
const emojiSpan = document.createElement('span');
emojiSpan.className = 'process-emoji';
emojiSpan.textContent = emojis;
container.appendChild(emojiSpan);
});
li.appendChild(container);
const roleList = document.createElement('ul');
roleList.className = 'role-list';
(roleList as HTMLElement).style.display = 'none';
// Traiter chaque rôle
Object.entries(roles).forEach(([roleName, roleData]: [string, any]) => {
const roleItem = document.createElement('li');
roleItem.className = 'role-item';
const roleContainer = document.createElement('div');
roleContainer.className = 'role-item-container';
const roleNameSpan = document.createElement('span');
roleNameSpan.className = 'role-name';
roleNameSpan.textContent = roleName;
// Filtrer les membres dupliqués ici, avant de les passer à toggleMembers
const uniqueMembers = new Map<string, any>();
roleData.members?.forEach((member: any) => {
const spAddress = member.sp_addresses?.[0];
if (spAddress && !uniqueMembers.has(spAddress)) {
uniqueMembers.set(spAddress, member);
}
});
// Créer un nouveau roleData avec les membres uniques
const filteredRoleData = {
...roleData,
members: Array.from(uniqueMembers.values())
};
roleContainer.addEventListener('click', async (event) => {
console.log("CLICKED ON ROLE:", roleName);
event.stopPropagation();
await this.toggleMembers(filteredRoleData, roleItem);
});
roleContainer.appendChild(roleNameSpan);
roleItem.appendChild(roleContainer);
roleList.appendChild(roleItem);
});
li.appendChild(roleList);
groupList.appendChild(li);
container.addEventListener('click', (event) => {
event.stopPropagation();
container.classList.toggle('expanded');
roleList.style.display = container.classList.contains('expanded') ? 'block' : 'none';
});
}
// Send a file
private async sendFile(file: File) {
const MAX_FILE_SIZE = 1 * 1024 * 1024;
@ -1782,30 +1717,28 @@ class ChatElement extends HTMLElement {
}
async connectedCallback() {
this.processId = this.getAttribute('process-id');
const service = await Services.getInstance();
if (this.processId) {
console.log("🔍 Chargement du chat avec processID");
await this.loadGroupListFromAProcess(this.processId);
} else {
const loadPage = async () => {
console.log("🔍 Chargement des processus par défaut");
const processSet = await this.getProcessesWhereTheCurrentMemberIs();
await this.loadAllProcesses(processSet);
}
if (this.selectedMember && this.selectedMember.length > 0) {
console.log('🔍 Loading chat for selected member:', this.selectedMember);
await this.loadMemberChat(this.selectedMember);
} else {
console.warn('⚠️ No member selected yet. Waiting for selection...');
await this.loadAllProcesses();
if (this.selectedMember) {
console.log('🔍 Loading chat for selected member:', this.selectedMember);
await this.loadMemberChat(this.selectedMember);
} else {
console.warn('⚠️ No member selected yet. Waiting for selection...');
}
}
let timeout: NodeJS.Timeout;
window.addEventListener('process-updated', async (e: CustomEvent) => {
const processId = e.detail.processId;
if (processId === this.processId) {
setTimeout(async () => await this.reloadMemberChat(this.selectedMember), 3000);
}
console.log('Notified of an update for process', processId);
await loadPage();
});
await loadPage();
}
}

View File

@ -4,24 +4,23 @@
<div class="tab-container">
<div class="tabs">
<div class="tab active" data-tab="tab1">Scan QR Code</div>
<div class="tab" data-tab="tab2">Scan other device</div>
<div class="tab active" data-tab="tab1">Create an account</div>
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
</div>
</div>
<div class="page-container">
<div id="tab1" class="card tab-content active">
<div class="card-description">Scan with your other device :</div>
<div class="card-description">Create an account :</div>
<div class="pairing-request"></div>
<div class="card-image qr-code">
<!-- <div class="card-image qr-code">
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
</div>
<button id="copyBtn" class="sp-address-btn"></button>
<div class="card-image emoji-display" id="emoji-display"></div>
</div> -->
<button id="createButton" class="create-btn"></button>
</div>
<div class="separator"></div>
<div id="tab2" class="card tab-content">
<div class="card-description">Scan your other device :</div>
<div class="card-description">Add a device for an existing member :</div>
<div class="card-image camera-card">
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
<button id="scan-btn" onclick="scanDevice()">Scan</button>
@ -31,8 +30,13 @@
</div>
</div>
<p>Or</p>
<input type="text" id="addressInput" placeholder="Paste address" />
<div id="emoji-display-2"></div>
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
<div id="emoji-display-2"></div> -->
<div class="card-description">Chose a member :</div>
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
<!-- Options -->
</select>
<button id="okButton" style="display: none">OK</button>
</div>
</div>

View File

@ -1,7 +1,7 @@
import Routing from '../../services/modal.service';
import Services from '../../services/service';
import { addSubscription } from '../../utils/subscription.utils';
import { displayEmojis, generateQRCode } from '../../utils/sp-address.utils';
import { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/html.utils';
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
export { QrScannerComponent };
@ -20,8 +20,12 @@ export async function initHomePage(): Promise<void> {
const service = await Services.getInstance();
const spAddress = await service.getDeviceAddress();
generateQRCode(spAddress);
// generateQRCode(spAddress);
generateCreateBtn ();
displayEmojis(spAddress);
// Add this line to populate the select when the page loads
await populateMemberSelect();
}
//// Modal
@ -45,4 +49,46 @@ function scanDevice() {
if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';
}
async function populateMemberSelect() {
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
if (!memberSelect) {
console.error('Could not find memberSelect element');
return;
}
const service = await Services.getInstance();
const members = service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const process = await service.getProcess(processId);
let memberPublicName;
if (process) {
const publicMemberData = service.getPublicData(process);
if (publicMemberData) {
const extractedName = publicMemberData['memberPublicName'];
if (extractedName !== undefined && extractedName !== null) {
memberPublicName = extractedName;
}
}
}
if (!memberPublicName) {
memberPublicName = 'Unnamed Member';
}
// Récupérer les emojis pour ce processId
const emojis = await addressToEmoji(processId);
const option = document.createElement('option');
option.value = processId;
option.textContent = `${memberPublicName} (${emojis})`;
memberSelect.appendChild(option);
}
}
(window as any).populateMemberSelect = populateMemberSelect;
(window as any).scanDevice = scanDevice;

View File

@ -164,10 +164,9 @@ async function populateAutocompleteList(select: HTMLSelectElement, query: string
let options_to_show = [];
const service = await Services.getInstance();
const myProcesses = await service.getMyProcesses();
const allProcesses = new Set(Object.keys(await service.getProcesses()));
const mineArray = Array.from(myProcesses);
const allArray = Array.from(allProcesses.difference(myProcesses));
const mineArray: string[] = await service.getMyProcesses();
const allProcesses = await service.getProcesses();
const allArray: string[] = Object.keys(allProcesses).filter(x => !mineArray.includes(x));
const wrapper = select.parentNode;
const input_search = wrapper?.querySelector('.search-container');
@ -191,7 +190,7 @@ async function populateAutocompleteList(select: HTMLSelectElement, query: string
mineArray.forEach(processId => addProcessToList(processId, true));
allArray.forEach(processId => addProcessToList(processId, false));
if (myProcesses.size === 0 && allProcesses.size === 0) {
if (mineArray.length === 0 && allArray.length === 0) {
const li = document.createElement('li');
li.classList.add('not-cursor');
li.innerText = 'No options found';
@ -480,7 +479,7 @@ async function createMessagingProcess(): Promise<void> {
await service.handleApiReturn(createProcessReturn);
const createPrdReturn = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(createPrdReturn);
const approveChangeReturn = service.approveChange(processId, stateId);
const approveChangeReturn = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveChangeReturn);
}, 500)
}

View File

@ -146,14 +146,9 @@ export async function init(): Promise<void> {
}
await services.restoreProcessesFromDB();
await services.restoreSecretsFromDB();
setTimeout(async () => {
const myProcesses = await services.getMyProcesses();
const db = await Database.getInstance();
await db.updateMyProcesses(myProcesses);
}, 200);
if (services.isPaired()) {
await navigate('process');
await navigate('chat');
} else {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);

View File

@ -1,5 +1,4 @@
let processesToScan = new Set();
let toDownload = new Set();
const EMPTY32BYTES = String('').padStart(64, '0');
self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting()); // Activate worker immediately
@ -12,82 +11,24 @@ self.addEventListener('activate', (event) => {
// Event listener for messages from clients
self.addEventListener('message', async (event) => {
const data = event.data;
if (data.type === 'START') {
const fetchNotifications = async () => {
const itemsWithFlag = await getAllDiffsNeedValidation();
// Process items with the specific flag
itemsWithFlag?.forEach((item) => {
console.log(item); // Do something with each flagged item
});
event.ports[0].postMessage({
type: 'NOTIFICATIONS',
data: itemsWithFlag,
});
};
const scanMissingData = async () => {
console.log('Scanning for missing data...');
const myProcesses = await getProcesses(processesToScan);
// Iterate on each process
if (myProcesses && myProcesses.length != 0) {
for (const process of myProcesses) {
// Iterate on states
const firstState = process.states[0];
const processId = firstState.commited_in;
for (const state of process.states) {
if (!state.pcd_commitment) continue;
// iterate on pcd_commitment
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
// Check if we have the data in db
const existingData = await getBlob(hash);
if (!existingData) {
toDownload.add(hash);
// We also add an entry in diff, in case it doesn't already exist
await addDiff(processId, state.state_id, hash, field)
} else {
// We remove it if we have it in the set
if (toDownload.delete(hash)) {
console.log(`Removing ${hash} from the set`);
}
}
}
}
}
}
if (toDownload.size != 0) {
event.ports[0].postMessage({
type: 'TO_DOWNLOAD',
data: Array.from(toDownload),
});
}
}
fetchNotifications();
setInterval(fetchNotifications, 2 * 60 * 1000);
scanMissingData();
setInterval(scanMissingData, 10 * 1000);
}
console.log(data);
if (data.type === 'UPDATE_PROCESSES') {
if (data.type === 'SCAN') {
try {
const { myProcessesId } = data.payload;
console.log(myProcessesId);
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length != 0) {
for (const processId of myProcessesId) {
processesToScan.add(processId);
const toDownload = await scanMissingData(myProcessesId);
if (toDownload.length != 0) {
console.log('Sending TO_DOWNLOAD message');
event.source.postMessage({ type: 'TO_DOWNLOAD', data: toDownload});
}
console.log(processesToScan);
} else {
event.ports[0].postMessage({ status: 'error', message: 'Empty lists' });
event.source.postMessage({ status: 'error', message: 'Empty lists' });
}
} catch (error) {
event.ports[0].postMessage({ status: 'error', message: error.message });
event.source.postMessage({ status: 'error', message: error.message });
}
}
if (data.type === 'ADD_OBJECT') {
} else if (data.type === 'ADD_OBJECT') {
try {
const { storeName, object, key } = data.payload;
const db = await openDatabase();
@ -107,6 +48,44 @@ self.addEventListener('message', async (event) => {
}
});
async function scanMissingData(processesToScan) {
console.log('Scanning for missing data...');
const myProcesses = await getProcesses(processesToScan);
let toDownload = new Set();
// Iterate on each process
if (myProcesses && myProcesses.length != 0) {
for (const process of myProcesses) {
// Iterate on states
const firstState = process.states[0];
const processId = firstState.commited_in;
for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue;
// iterate on pcd_commitment
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
// Skip public fields
if (state.public_data[field] !== undefined || field === 'roles') continue;
// Check if we have the data in db
const existingData = await getBlob(hash);
if (!existingData) {
toDownload.add(hash);
// We also add an entry in diff, in case it doesn't already exist
await addDiff(processId, state.state_id, hash, state.roles, field);
} else {
// We remove it if we have it in the set
if (toDownload.delete(hash)) {
console.log(`Removing ${hash} from the set`);
}
}
}
}
}
}
console.log(toDownload);
return Array.from(toDownload);
}
async function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('4nk', 1);
@ -246,7 +225,7 @@ async function getBlob(hash) {
return result;
}
async function addDiff(processId, stateId, hash) {
async function addDiff(processId, stateId, hash, roles, field) {
const db = await openDatabase();
const storeName = 'diffs';
const tx = db.transaction(storeName, 'readwrite');
@ -264,7 +243,8 @@ async function addDiff(processId, stateId, hash) {
process_id: processId,
state_id: stateId,
value_commitment: hash,
field: '',
roles: roles,
field: field,
description: null,
previous_value: null,
new_value: null,

View File

@ -8,6 +8,7 @@ export class Database {
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel | null = null;
private serviceWorkerCheckIntervalId: number | null = null;
private storeDefinitions = {
AnkLabels: {
name: 'labels',
@ -81,12 +82,10 @@ export class Database {
});
};
request.onsuccess = () => {
setTimeout(() => {
this.db = request.result;
this.initServiceWorker();
resolve();
}, 500);
request.onsuccess = async () => {
this.db = request.result;
await this.initServiceWorker();
resolve();
};
request.onerror = () => {
@ -115,43 +114,45 @@ export class Database {
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
try {
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length === 0) {
// No existing workers: register a new one.
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', registration.scope);
} else if (registrations.length === 1) {
// One existing worker: update it (restart it) without unregistering.
this.serviceWorkerRegistration = registrations[0];
await this.serviceWorkerRegistration.update();
console.log('Service worker updated');
} else {
// More than one existing worker: unregister them all and register a new one.
console.log('Multiple Service Worker(s) detected. Unregistering all...');
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All previous Service Workers unregistered.');
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', registration.scope);
}
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length === 0) {
// No existing workers: register a new one.
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
} else if (registrations.length === 1) {
// One existing worker: update it (restart it) without unregistering.
this.serviceWorkerRegistration = registrations[0];
await this.serviceWorkerRegistration.update();
console.log('Service Worker updated');
} else {
// More than one existing worker: unregister them all and register a new one.
console.log('Multiple Service Worker(s) detected. Unregistering all...');
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All previous Service Workers unregistered.');
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
}
await this.checkForUpdates();
await this.checkForUpdates();
// Set up the message channels
this.messageChannel = new MessageChannel();
this.messageChannelForGet = new MessageChannel();
this.messageChannel.port1.onmessage = this.handleAddObjectResponse;
this.messageChannelForGet.port1.onmessage = this.handleGetObjectResponse;
// Set up a global message listener for responses from the service worker.
navigator.serviceWorker.addEventListener('message', async (event) => {
console.log('Received message from service worker:', event.data);
await this.handleServiceWorkerMessage(event.data);
});
// Ensure the new service worker is activated before sending messages
// Set up a periodic check to ensure the service worker is active and to send a SYNC message.
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker = this.serviceWorkerRegistration.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration));
activeWorker?.postMessage(
{ type: 'START' },
[this.messageChannel.port2],
);
const service = await Services.getInstance();
const payload = await service.getMyProcesses();
if (payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
}
}, 5000);
} catch (error) {
console.error('Service Worker registration failed:', error);
console.error('Service Worker registration failed:', error);
}
}
@ -188,6 +189,58 @@ export class Database {
}
}
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case 'TO_DOWNLOAD':
await this.handleDownloadList(message.data);
break;
default:
console.warn('Unknown message type received from service worker:', message);
}
}
private async handleDownloadList(downloadList: string[]): void {
// Download the missing data
let requestedStateId = [];
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
if (!diff) {
// This should never happen
console.warn(`Missing a diff for hash ${hash}`);
continue;
}
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(new CustomEvent('newDataReceived', {
detail: {
processId,
stateId,
hash,
}
}));
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
}
private handleAddObjectResponse = async (event: MessageEvent) => {
const data = event.data;
console.log('Received response from service worker (ADD_OBJECT):', data);
@ -195,6 +248,7 @@ export class Database {
if (data.type === 'NOTIFICATIONS') {
service.setNotifications(data.data);
} else if (data.type === 'TO_DOWNLOAD') {
console.log(`Received missing data ${data}`);
// Download the missing data
let requestedStateId = [];
for (const hash of data.data) {
@ -211,8 +265,9 @@ export class Database {
const diff = await service.getDiffByValue(hash);
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, stateId);
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
@ -220,8 +275,6 @@ export class Database {
console.error(e);
}
}
// try to update list of my processes
await service.getMyProcesses();
}
};
@ -267,49 +320,6 @@ export class Database {
});
}
public updateMyProcesses(myProcessesId: string[]): Promise<void> {
if (myProcessesId.length === 0) {
return;
}
return new Promise(async (resolve, reject) => {
if (!this.serviceWorkerRegistration) {
// console.warn('Service worker registration is not ready. Waiting...');
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
}
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
// Create a message channel for communication
const messageChannel = new MessageChannel();
// Handle the response from the service worker
messageChannel.port1.onmessage = (event) => {
if (event.data.status === 'success') {
resolve();
} else {
const error = event.data.message;
reject(new Error(error || 'Unknown error occurred while scanning our processes'));
}
};
try {
const payload = { myProcessesId };
console.log('Sending UPDATE_PROCESSES msg with payload', payload);
activeWorker?.postMessage(
{
type: 'UPDATE_PROCESSES',
payload,
},
[messageChannel.port2],
);
} catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`));
}
});
}
public async getObject(storeName: string, key: string): Promise<any | null> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');

View File

@ -14,6 +14,7 @@ export default class ModalService {
private processId: string | null = null;
private constructor() {}
private paired_addresses: string[] = [];
private modal: HTMLElement | null = null;
// Method to access the singleton instance of Services
public static async getInstance(): Promise<ModalService> {
@ -54,6 +55,21 @@ export default class ModalService {
}
}
async injectCreationModal(members: any[]) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text());
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/modal/confirmation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
}
}
// Device 1 wait Device 2
async injectWaitingModal() {
const container = document.querySelector('#containerId');
@ -106,32 +122,37 @@ export default class ModalService {
throw new Error('Must have exactly 1 member');
}
console.log("MEMBERS:", members);
// We take all the addresses except our own
const service = await Services.getInstance();
const localAddress = await service.getDeviceAddress();
for (const member of members) {
for (const address of member['sp_addresses']) {
if (address !== localAddress) {
this.paired_addresses.push(address);
if (member.sp_addresses) {
for (const address of member.sp_addresses) {
if (address !== localAddress) {
this.paired_addresses.push(address);
}
}
}
}
this.processId = processId;
this.stateId = stateId;
await this.injectModal(members);
const modal = document.getElementById('modal');
if (modal) modal.style.display = 'flex';
// const newScript = document.createElement('script');
// newScript.setAttribute('type', 'module');
// newScript.textContent = confirmationModalScript;
// document.head.appendChild(newScript).parentNode?.removeChild(newScript);
// Add correct text
if (members[0].sp_addresses.length === 1) {
await this.injectCreationModal(members);
this.modal = document.getElementById('creation-modal');
console.log("LENGTH:", members[0].sp_addresses.length);
} else {
await this.injectModal(members);
this.modal = document.getElementById('modal');
console.log("LENGTH:", members[0].sp_addresses.length);
}
if (this.modal) this.modal.style.display = 'flex';
// Close modal when clicking outside of it
window.onclick = (event) => {
const modal = document.getElementById('modal');
if (event.target === modal) {
if (event.target === this.modal) {
this.closeConfirmationModal();
}
};
@ -140,14 +161,12 @@ export default class ModalService {
console.log('=============> Confirm Login');
}
async closeLoginModal() {
const modal = document.getElementById('login-modal');
if (modal) modal.style.display = 'none';
if (this.modal) this.modal.style.display = 'none';
}
async confirmPairing() {
const service = await Services.getInstance();
const modal = document.getElementById('modal');
if (modal) modal.style.display = 'none';
if (this.modal) this.modal.style.display = 'none';
if (service.device1) {
console.log("Device 1 detected");
@ -166,7 +185,7 @@ export default class ModalService {
// We send confirmation that we validate the change
try {
const approveChangeReturn = service.approveChange(this.processId!, this.stateId!);
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn);
await this.injectWaitingModal();
@ -186,7 +205,7 @@ export default class ModalService {
const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice);
await service.saveDeviceInDatabase(newDevice);
navigate('process');
navigate('chat');
service.resetState();
} catch (e) {
@ -215,7 +234,7 @@ export default class ModalService {
// We send confirmation that we validate the change
try {
const approveChangeReturn = service.approveChange(this.processId!, this.stateId!);
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn);
} catch (e) {
throw e;
@ -229,14 +248,13 @@ export default class ModalService {
const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice);
await service.saveDeviceInDatabase(newDevice);
navigate('process');
navigate('chat');
}
}
async closeConfirmationModal() {
const service = await Services.getInstance();
await service.unpairDevice();
const modal = document.getElementById('modal');
if (modal) modal.style.display = 'none';
if (this.modal) this.modal.style.display = 'none';
}
}

View File

@ -15,17 +15,14 @@ const BASEURL = `https://demo.4nkweb.com`;
const BOOTSTRAPURL = [`${BASEURL}/ws/`];
const STORAGEURL = `${BASEURL}/storage`
const DEFAULTAMOUNT = 1000n;
const EMPTY32BYTES = String('').padStart(64, '0');
export default class Services {
private static initializing: Promise<Services> | null = null;
private static instance: Services;
private currentProcess: string | null = null;
private pendingUpdates: any | null = null;
private currentUpdateMerkleRoot: string | null = null;
private localAddress: string | null = null;
private pairedAddresses: string[] = [];
private sdkClient: any;
// private processes: IProcess[] | null = null;
private myProcesses: Set = new Set();
private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
@ -241,64 +238,7 @@ export default class Services {
throw new Error('Amount is still 0 after 3 attempts');
}
public async createMessagingProcess(otherMembers: Member[],relayAddress: string, feeRate: number): Promise<ApiReturn> {
if (!this.isPaired()) {
throw new Error('Device not paired');
}
const me = await this.getMemberFromDevice();
if (!me) {
throw new Error('No paired member in device');
}
const allMembers: Member[] = otherMembers;
const meAndOne = [{ sp_addresses: me }, otherMembers.pop()!];
allMembers.push({ sp_addresses: me });
const everyOneElse = otherMembers;
const messagingTemplate = {
description: 'messaging',
roles: {
public: {
members: allMembers,
validation_rules: [
{
quorum: 0.0,
fields: ['description', 'roles'],
min_sig_member: 0.0,
},
],
storages: [STORAGEURL]
},
owner: {
members: meAndOne,
validation_rules: [
{
quorum: 1.0,
fields: ['description', 'roles'],
min_sig_member: 1.0,
},
],
storages: [STORAGEURL]
},
users: {
members: everyOneElse,
validation_rules: [
{
quorum: 0.0,
fields: ['description', 'roles'],
min_sig_member: 0.0,
},
],
storages: [STORAGEURL]
},
},
};
try {
return this.sdkClient.create_new_process(JSON.stringify(messagingTemplate), null, relayAddress, feeRate);
} catch (e) {
throw new Error(`Creating process failed: ${e}`);
}
}
public async createPairingProcess(pairWith: string[], relayAddress: string, feeRate: number): Promise<ApiReturn> {
public async createPairingProcess(userName: string, pairWith: string[], relayAddress: string, feeRate: number): Promise<ApiReturn> {
if (this.sdkClient.is_paired()) {
throw new Error('Device already paired');
}
@ -310,7 +250,7 @@ export default class Services {
validation_rules: [
{
quorum: 1.0,
fields: ['description', 'counter'],
fields: ['description', 'counter', 'roles', 'memberPublicName'],
min_sig_member: 1.0,
},
],
@ -321,11 +261,14 @@ export default class Services {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName
}
try {
return this.sdkClient.create_new_process(
JSON.stringify(pairingTemplate),
JSON.stringify(roles),
null,
pairingTemplate,
roles,
publicData,
relayAddress,
feeRate
);
@ -356,6 +299,19 @@ export default class Services {
}
const roles = {
demiurge: {
members: [
{ sp_addresses: myAddresses },
],
validation_rules: [
{
quorum: 0.01,
fields: ['message', 'description', 'roles'],
min_sig_member: 0.01,
},
],
storages: [STORAGEURL]
}
dm: {
members: [
{ sp_addresses: myAddresses },
@ -381,14 +337,14 @@ export default class Services {
const relayAddress = this.getAllRelays()[0]['spAddress'];
const feeRate = 1;
const initState = JSON.stringify(dmTemplate);
const publicData = {};
await this.checkConnections ([{ sp_addresses: otherMember }]);
const result = this.sdkClient.create_new_process (
initState,
JSON.stringify(roles),
null,
dmTemplate,
roles,
publicData,
relayAddress,
feeRate
);
@ -404,7 +360,7 @@ export default class Services {
public async updateProcess(process: Process, new_state: any, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> {
// If roles is null, we just take the last commited state roles
if (!roles) {
roles = await this.getRoles(process);
roles = this.getRoles(process);
} else {
// We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm
console.log('Provided new roles:', JSON.stringify(roles));
@ -420,7 +376,7 @@ export default class Services {
await this.checkConnections([...members]);
try {
console.log(process);
return this.sdkClient.update_process(process, new_state, roles);
return this.sdkClient.update_process(process, new_state, roles, {});
} catch (e) {
throw new Error(`Failed to update process: ${e}`);
}
@ -447,21 +403,25 @@ export default class Services {
}
}
public approveChange(processId: string, stateId: string): ApiReturn {
public async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
const process = await this.getProcess(processId);
if (!process) {
throw new Error('Failed to get process from db');
}
try {
return this.sdkClient.validate_state(processId, stateId);
return this.sdkClient.validate_state(process, stateId);
} catch (e) {
throw new Error(`Failed to create prd response: ${e}`);
}
}
public rejectChange(): ApiReturn {
if (!this.currentProcess || !this.currentUpdateMerkleRoot) {
throw new Error('No current process and/or current update defined');
public async rejectChange(processId: string, stateId: string): Promise<ApiReturn> {
const process = await this.getProcess(processId);
if (!process) {
throw new Error('Failed to get process from db');
}
try {
return this.sdkClient.refuse_state(this.currentProcess, this.currentUpdateMerkleRoot);
return this.sdkClient.refuse_state(process, stateId);
} catch (e) {
throw new Error(`Failed to create prd response: ${e}`);
}
@ -534,16 +494,6 @@ export default class Services {
}
}
public async updateProcessesWorker() {
try {
const myProcesses = await this.getMyProcesses();
const db = await Database.getInstance();
await db.updateMyProcesses(myProcesses);
} catch (e) {
console.error('Failed to update processes worker:', e);
}
}
public async handleApiReturn(apiReturn: ApiReturn) {
console.log(apiReturn);
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
@ -601,14 +551,6 @@ export default class Services {
// Save process to db
await this.saveProcessToDb(processId, updatedProcess.current_process);
setTimeout(async () => {
try {
await this.updateProcessesWorker();
} catch (e) {
console.error(e);
}
}, 0)
const isPaired = this.isPaired();
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
@ -681,6 +623,15 @@ export default class Services {
}
}
public dumpNeuteredDevice(): Device | null {
try {
return this.sdkClient.dump_neutered_device();
} catch (e) {
console.error(`Failed to dump device: ${e}`);
return null;
}
}
public getPairingProcessId(): string {
try {
return this.sdkClient.get_pairing_process_id();
@ -749,24 +700,27 @@ export default class Services {
return true;
}
rolesContainsUs(roles: any): boolean {
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try {
this.sdkClient.roles_contains_us(JSON.stringify(roles));
us = this.sdkClient.get_member();
} catch (e) {
return false;
throw e;
}
return true;
return this.rolesContainsMember(roles, us.sp_addresses);
}
rolesContainsMember(roles: any, member: string[]): boolean {
try {
this.sdkClient.roles_contains_member(JSON.stringify(roles), member);
} catch (e) {
return false;
rolesContainsMember(roles: Record<string, RoleDefinition>, member: string[]): boolean {
let res = false;
for (const [roleName, roleDef] of Object.entries(roles)) {
for (const otherMember of roleDef.members) {
if (res) { return true }
res = this.compareMembers(member, otherMember.sp_addresses);
}
}
return true;
return res;
}
async dumpWallet() {
@ -993,21 +947,27 @@ export default class Services {
}
}
async decryptAttribute(state: ProcessState, attribute: string): Promise<string | null> {
let hash;
let key;
try {
hash = state.pcd_commitment[attribute];
} catch (e) {
console.error(`Failed to find hash for attribute ${attribute}`);
return null;
}
try {
key = state.keys[attribute];
} catch (e) {
console.error(`Failed to find key for attribute ${attribute}`);
return null;
async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<string | null> {
let hash = state.pcd_commitment[attribute];
let key = state.keys[attribute];
// If hash or key is missing, request an update and then retry
if (!hash || !key) {
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds
let retries = 0;
while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting
hash = state.pcd_commitment[attribute];
key = state.keys[attribute];
retries++;
}
}
if (hash && key) {
const blob = await this.getBlobFromDb(hash);
if (blob) {
@ -1019,12 +979,12 @@ export default class Services {
const clear = this.sdkClient.decrypt_data(new Uint8Array(keyBuf), cipher);
if (clear) {
// This is stringified json, we parse it back
// Parse the stringified JSON
return JSON.parse(clear);
}
}
}
return null;
}
@ -1135,12 +1095,23 @@ export default class Services {
for (const [processId, process] of Object.entries(newProcesses)) {
const existing = await this.getProcess(processId);
if (existing) {
console.log(`${processId} already in db`);
// Look for state id we don't know yet
let new_states = [];
let roles = [];
for (const state of process.states) {
if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; }
if (!this.lookForStateId(existing, state.state_id)) {
new_states.push(state.state_id);
roles.push(state.roles);
}
}
const event = new CustomEvent('process-updated', {
detail: { processId }
});
window.dispatchEvent(event);
if (new_states.length != 0) {
// We request the new states
await this.requestDataFromPeers(processId, new_states, roles);
}
// Otherwise we're probably just in the initial loading at page initialization
// We may learn an update for this process
// TODO maybe actually check if what the relay is sending us contains more information than what we have
@ -1152,7 +1123,6 @@ export default class Services {
await this.saveProcessToDb(processId, process as Process);
}
}
await this.updateProcessesWorker();
}
}, 500)
} catch (e) {
@ -1160,23 +1130,50 @@ export default class Services {
}
}
private lookForStateId(process: Process, stateId: string): boolean {
for (const state of process.states) {
if (state.state_id === stateId) {
return true;
}
}
return false;
}
/**
* Retourne la liste de tous les membres
* Retourne la liste de tous les membres ordonnés par leur process id
* @returns Un tableau contenant tous les membres
*/
public getAllMembers(): Record<string, Member> {
public getAllMembersSorted(): Record<string, Member> {
return Object.fromEntries(
Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
);
}
public getAllMembers(): Record<string, Member> {
return this.membersList;
}
public getAddressesForMemberId(memberId: string): string[] | null {
return this.membersList[memberId];
}
public compareMembers(memberA: string[], memberB: string[]): boolean {
if (!memberA || !memberB) { return false }
if (memberA.length !== memberB.length) { return false }
const res = memberA.every(item => memberB.includes(item)) && memberB.every(item => memberA.includes(item));
return res;
}
public async handleCommitError(response: string) {
const content = JSON.parse(response);
const error = content.error;
const errorMsg = error['GenericError'];
if (errorMsg === 'State is identical to the previous state') {
return;
} else if (errorMsg === 'Not enough valid proofs') { return; }
// Wait and retry
setTimeout(async () => {
await this.sendCommitMessage(JSON.stringify(content));
@ -1184,7 +1181,7 @@ export default class Services {
}
public async getRoles(process: Process): Promise<record<string, RoleDefinition> | null> {
public getRoles(process: Process): Record<string, RoleDefinition> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
return lastCommitedState!.roles;
@ -1193,37 +1190,56 @@ export default class Services {
}
}
async getMyProcesses(): Promise<Set<string>> {
public getPublicData(process: Process): Record<string, any> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) {
return lastCommitedState.public_data;
} else {
return null;
}
}
public getProcessName(process: Process): string | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) {
const processName = lastCommitedState!.public_data['processName'];
if (processName) { return processName }
else { return null }
} else {
return null;
}
}
public async getMyProcesses(): Promise<string[]> {
try {
const processes = await this.getProcesses();
const userProcessSet = new Set<string>();
for (const [processId, process] of Object.entries(processes)) {
// We use myProcesses attribute to not reevaluate all processes everytime
if (this.myProcesses && this.myProcesses.has(processId)) {
userProcessSet.add(processId);
continue;
}
try {
const roles = await this.getRoles(process);
const roles = this.getRoles(process);
if (this.rolesContainsUs(roles)) {
if (roles && this.rolesContainsUs(roles)) {
this.myProcesses.add(processId);
userProcessSet.add(processId);
}
} catch (e) {
console.error(e);
}
}
return userProcessSet;
return Array.from(this.myProcesses);
} catch (e) {
console.error("Failed to get processes:", e);
}
}
public async requestDataFromPeers(processId: string, stateId: string) {
public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record<string, RoleDefinition>[]) {
console.log('Requesting data from peers');
console.log(roles);
try {
const res = this.sdkClient.request_data(processId, [stateId]);
const res = this.sdkClient.request_data(processId, stateIds, roles);
await this.handleApiReturn(res);
} catch (e) {
console.error(e);
@ -1261,4 +1277,15 @@ export default class Services {
return null;
}
}
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
if (Object.keys(roles).length != 1) { return false }
const pairingRole = roles['pairing'];
if (pairingRole) {
// For now that's enough, we should probably test more things
return true;
} else {
return false;
}
}
}

View File

@ -104,6 +104,7 @@ export function initAddressInput() {
const addressInput = container.querySelector('#addressInput') as HTMLInputElement;
const emojiDisplay = container.querySelector('#emoji-display-2');
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
addSubscription(addressInput, 'input', async () => {
let address = addressInput.value;
@ -145,6 +146,12 @@ export function initAddressInput() {
onOkButtonClick();
});
}
if (createButton) {
addSubscription(createButton, 'click', () => {
onCreateButtonClick();
});
}
}
async function onOkButtonClick() {
@ -158,21 +165,46 @@ async function onOkButtonClick() {
}
}
export async function prepareAndSendPairingTx(secondDeviceAddress: string) {
async function onCreateButtonClick() {
try {
await prepareAndSendPairingTx();
} catch (e) {
console.error(`onCreateButtonClick error: ${e}`);
}
}
export async function prepareAndSendPairingTx(promptName: boolean = false) {
const service = await Services.getInstance();
// Device 1 wait Device 2
service.device1 = true;
// service.device1 = true;
try {
await service.checkConnections([{ sp_addresses: [secondDeviceAddress] }]);
await service.checkConnections([]);
} catch (e) {
throw e;
}
// Create the process
// Prompt the user for a username.
let userName;
if (promptName) {
userName = prompt("Please enter your user name:");
} else {
userName = "";
}
// Create the process after a delay.
setTimeout(async () => {
const relayAddress = service.getAllRelays();
const createPairingProcessReturn = await service.createPairingProcess([secondDeviceAddress], relayAddress[0].spAddress, 1);
const relayAddress = service.getAllRelays();
// Pass the userName as an additional parameter.
const createPairingProcessReturn = await service.createPairingProcess(
userName,
[],
relayAddress[0].spAddress,
1,
userName
);
if (!createPairingProcessReturn.updated_process) {
throw new Error('createPairingProcess returned an empty new process'); // This should never happen
@ -189,17 +221,21 @@ export async function generateQRCode(spAddress: string) {
const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress);
const qrCode = container?.querySelector('.qr-code img');
qrCode?.setAttribute('src', url);
//Generate Address CopyBtn
const address = container?.querySelector('.sp-address-btn');
if (address) {
address.textContent = 'Copy address';
}
const copyBtn = container.querySelector('#copyBtn');
if (copyBtn) {
addSubscription(copyBtn, 'click', () => copyToClipboard(currentUrl + '?sp_address=' + spAddress));
}
} catch (err) {
console.error(err);
}
}
export async function generateCreateBtn() {
try{
//Generate CreateBtn
const container = getCorrectDOM('login-4nk-component') as HTMLElement
const createBtn = container?.querySelector('.create-btn');
if (createBtn) {
createBtn.textContent = 'CREATE';
}
} catch (err) {
console.error(err);
}
}