declare global { interface Window { loadMemberChat: (memberId: string | number) => void; } } import { membersMock } from '../../mocks/mock-signature/membersMocks'; import { ApiReturn, Device, Member, Process, RoleDefinition } from '../../../pkg/sdk_client'; import { getCorrectDOM } from '../../utils/document.utils'; import chatStyle from '../../../public/style/chat.css?inline'; import { addressToEmoji } from '../../utils/sp-address.utils'; import Database from '../../services/database.service'; import Services from '../../services/service'; const storageUrl = `/storage`; interface LocalNotification { memberId: string; text: string; time: string; } export function initChat() { const chatElement = document.createElement('chat-element'); const container = document.querySelector('.container'); if (container) { container.appendChild(chatElement); } } class ChatElement extends HTMLElement { static get observedAttributes() { return ['process-id']; } private processId: string | null = null; private processRoles: any | null = null; private selectedMember: string | null = null; private notifications: LocalNotification[] = []; private notificationBadge = document.querySelector('.notification-badge'); private notificationBoard = document.getElementById('notification-board'); private notificationBell = document.getElementById('notification-bell'); private allMembers = membersMock.map(member => ({ id: member.id, name: member.name, roleName: 'Default Role' })); private messageState: number = 0; private selectedRole: string | null = null; private userProcessSet: Set = new Set(); private dmMembersSet: Set = new Set(); private addressMap: Record = {}; constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot!.innerHTML = `

Signature

Description

Documents

    `; window.loadMemberChat = async (memberId: string | number) => { if (typeof memberId === 'string') { return await this.loadMemberChat(memberId); } else { console.error('Invalid memberId type. Expected string, got number.'); } }; document.addEventListener('DOMContentLoaded', () => { this.notificationBadge = document.querySelector('.notification-badge'); this.notificationBoard = document.getElementById('notification-board'); this.notificationBell = document.getElementById('notification-bell'); if (!this.notificationBadge || !this.notificationBoard || !this.notificationBell) { console.error('Notification elements not found'); } }); // Initialiser les événements de notification document.addEventListener('click', (event: Event): void => { if (this.notificationBoard && this.notificationBoard.style.display === 'block' && !this.notificationBoard.contains(event.target as Node) && this.notificationBell && !this.notificationBell.contains(event.target as Node)) { this.notificationBoard.style.display = 'none'; } }); this.initMessageEvents(); } 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(); setTimeout(async () => await this.reloadMemberChat(this.selectedMember), 600); messageInput.value = ''; }); } const messageInput = this.shadowRoot?.querySelector('#message-input'); if (messageInput) { messageInput.addEventListener('keypress', (event: Event) => { const keyEvent = event as KeyboardEvent; if (keyEvent.key === 'Enter' && !keyEvent.shiftKey) { event.preventDefault(); this.sendMessage(); setTimeout(async () => await this.reloadMemberChat(this.selectedMember), 600); messageInput.value = ''; } }); } const fileInput = this.shadowRoot?.querySelector('#file-input') as HTMLInputElement; if (fileInput) { fileInput.addEventListener('change', (event: Event) => { const target = event.target as HTMLInputElement; if (target.files && target.files.length > 0) { this.sendFile(target.files[0]); } }); } } ///////////////////// Notification module ///////////////////// // Delete a notification private removeNotification(index: number) { this.notifications?.splice(index, 1); // Ajout de ?. this.renderNotifications(); this.updateNotificationBadge(); } // Show notifications private renderNotifications() { if (!this.notificationBoard) return; // Reset the interface this.notificationBoard.innerHTML = ''; // Displays "No notifications available" if there are no notifications if (this.notifications.length === 0) { this.notificationBoard.innerHTML = '
    No notifications available
    '; return; } // Add each notification to the list this.notifications.forEach((notif, index) => { const notifElement = document.createElement('div'); notifElement.className = 'notification-item'; notifElement.textContent = `${notif.text} at ${notif.time}`; notifElement.onclick = async () => { await this.loadMemberChat(notif.memberId); await this.removeNotification(index); }; this.notificationBoard?.appendChild(notifElement); }); } private updateNotificationBadge() { if (!this.notificationBadge) return; const count = this.notifications.length; this.notificationBadge.textContent = count > 99 ? '+99' : count.toString(); (this.notificationBadge as HTMLElement).style.display = count > 0 ? 'block' : 'none'; } // Add notification private async addNotification(memberId: string, message: any) { try { // Obtenir l'emoji à partir du Pairing Process const pairingProcess = await this.getPairingProcess(memberId); const memberEmoji = await addressToEmoji(pairingProcess); // Obtenir le processus et le rôle const processId = this.getAttribute('process-id'); const processEmoji = processId ? await addressToEmoji(processId) : '📝'; // Trouver le rôle du membre const member = this.allMembers.find(m => String(m.id) === memberId); const role = message.metadata?.roleName || 'Member'; // Déterminer le texte de la notification let notificationText = ''; if (message.type === 'file') { notificationText = `${memberEmoji} (${role}) in ${processEmoji}: New file - ${message.fileName}`; } else { notificationText = `${memberEmoji} (${role}) in ${processEmoji}: ${message.metadata.text}`; } // Créer la notification const notification = { memberId, text: notificationText, time: new Date(message.metadata.timestamp).toLocaleString('fr-FR') }; // Ajouter la notification et mettre à jour l'interface this.notifications.push(notification); this.renderNotifications(); this.updateNotificationBadge(); } catch (error) { console.error('Error creating notification:', error); } } private async sendMessage() { const messageInput = this.shadowRoot?.querySelector('#message-input') as HTMLInputElement; if (!messageInput || !this.selectedMember) { console.error('❌ Missing message input or selected member'); return; } if (!this.processId) { console.error('no process id set'); return; } const messageText = messageInput.value.trim(); if (messageText === '') { console.error('❌ Empty message'); return; } try { const service = await Services.getInstance(); const myProcessId = await this.getMyProcessId(); if (!myProcessId) { console.error('No paired member found'); return; } const timestamp = Date.now(); const message = { state: this.messageState, type: 'text', content: messageText, metadata: { createdAt: timestamp, lastModified: timestamp, sender: myProcessId, recipient: this.selectedMember, } }; console.log("----this.processId",this.processId ); const process = await service.getProcess(this.processId); if (!process) { console.error('Failed to retrieve process from DB'); return; } // For a dm process, there are only 2 attributes, description will stay the same, message is the new message // We don't need to get previous values for now, so let's just skip it let newState = { message: message, description: 'dm' }; // Now we create a new state for the dm process let apiReturn; try { console.log(process); apiReturn = await service.updateProcess(process, newState, null); } catch (e) { console.error('Failed to update process:', e); return; } const updatedProcess = apiReturn.updated_process.current_process; const newStateId = updatedProcess.states[updatedProcess.states.length - 2 ].state_id; // We take the last concurrent state, just before the tip console.log(`newStateId: ${newStateId}`); await service.handleApiReturn(apiReturn); const createPrdReturn = service.createPrdUpdate(this.processId, newStateId); await service.handleApiReturn(createPrdReturn); // Now we validate the new state const approveChangeReturn = service.approveChange(this.processId, newStateId); await service.handleApiReturn(approveChangeReturn); await this.loadMemberChat(this.selectedMember); } catch (error) { console.error('❌ Error in sendMessage:', error); } } private scrollToBottom(container: Element) { (container as HTMLElement).scrollTop = (container as HTMLElement).scrollHeight; } // Get the diff by state id async getDiffByStateId(stateId: string) { try { const database = await Database.getInstance(); const diff = await database.requestStoreByIndex('diffs', 'byStateId', stateId); return diff; } catch (error) { console.error('Error getting diff by state id:', error); } } private async lookForChildren(): Promise { // Filter processes for the children of current process const service = await Services.getInstance(); if (!this.processId) { console.error('No process id'); return null; } const children: string[] = await service.getChildrenOfProcess(this.processId); const processRoles = this.processRoles; const selectedMember = this.selectedMember; for (const child of children) { const roles = await 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') continue; } if (!service.rolesContainsMember(roles, selectedMember)) { console.error('Member is not part of the process'); continue; } if (!service.rolesContainsUs(roles)) { console.error('We\'re not part of child process'); continue; } return child; } return null; } private async loadAllMembers() { const groupList = this.shadowRoot?.querySelector('#group-list'); if (!groupList) return; const service = await Services.getInstance(); const members = await service.getAllMembers(); const database = await Database.getInstance(); const db = database.db; const memberList = document.createElement('ul'); memberList.className = 'member-list active'; const prioritizedMembers: [string, any][] = []; const remainingMembers: [string, any][] = []; for (const [processId, member] of Object.entries(members)) { if (this.dmMembersSet.has(processId)) { prioritizedMembers.push([processId, member]); } else { remainingMembers.push([processId, member]); } } const sortedMembers = prioritizedMembers.concat(remainingMembers); for (const [processId, member] of Object.entries(members)) { const memberItem = document.createElement('li'); memberItem.className = 'member-item'; 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)'; }; } const memberContainer = document.createElement('div'); memberContainer.className = 'member-container'; 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})`; }; memberContainer.appendChild(emojiSpan); memberItem.appendChild(memberContainer); memberItem.addEventListener('click', async () => { await this.loadMemberChat(processId); }); 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); }; }); memberList.appendChild(memberItem); memberContainer.appendChild(editLabelButton); } groupList.appendChild(memberList); } private async lookForDmProcess(): Promise { const service = await Services.getInstance(); const processes = await service.getMyProcesses(); console.log(processes); const recipientAddresses = await service.getAddressesForMemberId(this.selectedMember).sp_addresses; console.log(recipientAddresses); for (const processId of processes) { try { 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'); console.log(description); if (!description || description !== "dm") { continue; } const roles = await service.getRoles(process); if (!service.rolesContainsMember(roles, recipientAddresses)) { console.error('Member is not part of the process'); continue; } return processId; } catch (e) { console.error(e); } } return null; } private async lookForMyDms(): Promise { const service = await Services.getInstance(); const processes = await service.getMyProcesses(); const myAddresses = await service.getMemberFromDevice(); const allMembers = await service.getAllMembers(); this.dmMembersSet.clear(); try { for (const processId of processes) { const process = await service.getProcess(processId); const state = process.states[0]; const description = await service.decryptAttribute(state, 'description'); if (!description || description !== "dm") { continue; } const roles = await service.getRoles(process); if (!service.rolesContainsMember(roles, myAddresses)) { continue; } 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); } } } const updatedDmMembersSet = new Set(); 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); return null; } private async loadMemberChat(pairingProcess: string) { try { const service = await Services.getInstance(); const myAddresses = await service.getMemberFromDevice(); const database = await Database.getInstance(); const db = database.db; if (!myAddresses) { console.error('No paired member found'); return; } // Set the selected member this.selectedMember = pairingProcess; console.log("SELECTED MEMBER: ", this.selectedMember); const chatHeader = this.shadowRoot?.querySelector('#chat-header'); const messagesContainer = this.shadowRoot?.querySelector('#messages'); if (!chatHeader || !messagesContainer) return; const emojis = await addressToEmoji(pairingProcess); const transaction = db.transaction("labels", "readonly"); const store = transaction.objectStore("labels"); const request = store.get(emojis); request.onsuccess = () => { const label = request.result; 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.lookForDmProcess(); if (dmProcessId === null) { console.log('Create a new dm process'); // We need to create a new process try { const memberAddresses = await service.getAddressesForMemberId(this.selectedMember); console.log("MEMBER ADDRESSES: ", memberAddresses); // await service.checkConnections(otherMembers); const res = await service.createDmProcess(memberAddresses.sp_addresses); // We catch the new process here const updatedProcess = res.updated_process?.current_process; const processId = updatedProcess?.states[0]?.commited_in; const stateId = updatedProcess?.states[0]?.state_id; await service.handleApiReturn(res); setTimeout(async () => { // Now create a first commitment console.log('Created a dm process', processId); this.processId = processId; const createPrdReturn = await service.createPrdUpdate(processId, stateId); console.log(createPrdReturn); await service.handleApiReturn(createPrdReturn); const approveChangeReturn = service.approveChange(processId, stateId); console.log(approveChangeReturn); await service.handleApiReturn(approveChangeReturn); }, 500); } catch (e) { console.error(e); return; } while (dmProcessId === null) { dmProcessId = await this.lookForDmProcess(); await new Promise(r => setTimeout(r, 1000)); } } else { console.log('Found DM process', dmProcessId); this.processId = dmProcessId; } /* TODO console.log("Je suis messagesProcess", messagesProcess); // --- GET THE STATE ID --- const messagesProcessStateId = messagesProcess?.states?.[0]?.state_id; console.log("Je suis messagesProcessStateId", messagesProcessStateId); // --- GET THE DIFF FROM THE STATE ID --- if (messagesProcessStateId) { const diffFromStateId = await this.getDiffByStateId(messagesProcessStateId); console.log("Je suis diffFromStateId", diffFromStateId); }*/ // Récupérer les messages depuis les états du processus const allMessages: any[] = []; const dmProcess = await service.getProcess(dmProcessId); 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 (message === "" || message === undefined || message === null) { 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); for (const message of allMessages) { const messageElement = document.createElement('div'); messageElement.className = 'message-container'; const myProcessId = await this.getMyProcessId(); const isCurrentUser = message.metadata.sender === myProcessId; messageElement.style.justifyContent = isCurrentUser ? 'flex-end' : 'flex-start'; const messageContent = document.createElement('div'); messageContent.className = isCurrentUser ? 'message user' : 'message'; const myEmoji = await addressToEmoji(myProcessId); const otherEmoji = await addressToEmoji(this.selectedMember); const senderEmoji = isCurrentUser ? myEmoji : otherEmoji; if (message.type === 'file') { let fileContent = ''; if (message.content.type.startsWith('image/')) { fileContent = `
    Image
    `; } else { const blob = this.base64ToBlob(message.content.data, message.content.type); const url = URL.createObjectURL(blob); fileContent = ` `; } messageContent.innerHTML = `
    ${senderEmoji}: ${fileContent}
    ${new Date(message.metadata.createdAt).toLocaleString('fr-FR')}
    `; } else { messageContent.innerHTML = `
    ${senderEmoji}: ${message.content}
    ${new Date(message.metadata.createdAt).toLocaleString('fr-FR')}
    `; } messageElement.appendChild(messageContent); messagesContainer.appendChild(messageElement); } this.scrollToBottom(messagesContainer); } else { console.log('No messages found'); } this.scrollToBottom(messagesContainer); } catch (error) { console.error('❌ Error in loadMemberChat:', error); } } private async reloadMemberChat(pairingProcess: string) { try { const service = await Services.getInstance(); const database = await Database.getInstance(); const db = database.db; const chatHeader = this.shadowRoot?.querySelector('#chat-header'); const messagesContainer = this.shadowRoot?.querySelector('#messages'); if (!chatHeader || !messagesContainer) return; const emojis = await addressToEmoji(pairingProcess); const transaction = db.transaction("labels", "readonly"); const store = transaction.objectStore("labels"); const request = store.get(emojis); request.onsuccess = () => { const label = request.result; 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; // Récupérer les messages depuis les états du processus const allMessages: any[] = []; const dmProcess = await service.getProcess(dmProcessId); 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 (message === "" || message === undefined || message === null) { 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); for (const message of allMessages) { const messageElement = document.createElement('div'); messageElement.className = 'message-container'; const myProcessId = await this.getMyProcessId(); const isCurrentUser = message.metadata.sender === myProcessId; messageElement.style.justifyContent = isCurrentUser ? 'flex-end' : 'flex-start'; const messageContent = document.createElement('div'); messageContent.className = isCurrentUser ? 'message user' : 'message'; const myEmoji = await addressToEmoji(myProcessId); const otherEmoji = await addressToEmoji(this.selectedMember); const senderEmoji = isCurrentUser ? myEmoji : otherEmoji; if (message.type === 'file') { let fileContent = ''; if (message.content.type.startsWith('image/')) { fileContent = `
    Image
    `; } else { const blob = this.base64ToBlob(message.content.data, message.content.type); const url = URL.createObjectURL(blob); fileContent = ` `; } messageContent.innerHTML = `
    ${senderEmoji}: ${fileContent}
    ${new Date(message.metadata.createdAt).toLocaleString('fr-FR')}
    `; } else { messageContent.innerHTML = `
    ${senderEmoji}: ${message.content}
    ${new Date(message.metadata.createdAt).toLocaleString('fr-FR')}
    `; } messageElement.appendChild(messageContent); messagesContainer.appendChild(messageElement); } this.scrollToBottom(messagesContainer); } else { console.log('No messages found'); } this.scrollToBottom(messagesContainer); } catch (error) { console.error('❌ Error in reloadMemberChat:', error); } } private base64ToBlob(base64: string, type: string): Blob { const byteCharacters = atob(base64.split(',')[1]); const byteArrays = []; for (let offset = 0; offset < byteCharacters.length; offset += 512) { const slice = byteCharacters.slice(offset, offset + 512); const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } return new Blob(byteArrays, { type: type }); } //To get a map with key: sp address, value: pairing process async getAddressMap() { const service = await Services.getInstance(); const allMembers = await service.getAllMembers(); const addressMap: Record = {}; for (const [key, values] of Object.entries(allMembers)) { if (values.sp_addresses) { for (let value of values.sp_addresses) { this.addressMap[value] = key; } } else { console.log(`No sp_addresses array found for key "${key}"`); } } return this.addressMap; } async findProcessIdFromAddresses(addresses: string[]): Promise { console.log('Addresses to find:', addresses); const service = await Services.getInstance(); const allMembers = await service.getAllMembers(); console.log('Available members:', allMembers); const sortedAddresses = [...addresses].sort(); for (const [key, value] of Object.entries(allMembers)) { if (value.sp_addresses.length === sortedAddresses.length) { const sortedValue = [...value.sp_addresses].sort(); if (sortedValue.every((val, index) => val === sortedAddresses [index])) { return key; // Found a match } } } return null; // No match found } private async toggleMembers(roleData: any, roleElement: HTMLElement) { console.log('Toggle members called with roleData:', roleData); let memberList = roleElement.querySelector('.member-list'); const roleName = roleElement.querySelector('.role-name')?.textContent || ''; if (memberList) { console.log('Existing memberList found, toggling display'); (memberList as HTMLElement).style.display = (memberList as HTMLElement).style.display === 'none' ? 'block' : 'none'; return; } console.log('Creating new memberList'); memberList = document.createElement('ul'); memberList.className = 'member-list'; if (roleData.members) { console.log('Members found:', roleData.members); for (const member of roleData.members) { console.log('Processing member:', member); const memberItem = document.createElement('li'); memberItem.className = 'member-item'; const memberContainer = document.createElement('div'); memberContainer.className = 'member-container'; const emojiSpan = document.createElement('span'); emojiSpan.className = 'member-emoji'; const pairingProcess = await this.findProcessIdFromAddresses(member.sp_addresses); console.log('PairingProcess:', pairingProcess); if (pairingProcess) { //TO DO : faire apparaitre les membres avec lesquelels je suis pairé ? const emojis = await addressToEmoji(pairingProcess); console.log('Adresse pairée:', emojis); emojiSpan.textContent = emojis; } else { const emojis = await addressToEmoji(member.sp_addresses[0]); emojiSpan.textContent = emojis; } memberContainer.appendChild(emojiSpan); memberItem.appendChild(memberContainer); memberItem.onclick = async (event) => { event.stopPropagation(); try { if (pairingProcess) { await this.loadMemberChat(pairingProcess); } } catch (error) { console.error('❌ Error handling member click:', error); } }; memberList.appendChild(memberItem); } } else { console.log('No members found in roleData'); } roleElement.appendChild(memberList); } private async switchTab(tabType: string, tabs: NodeListOf) { // Mettre à jour les classes des onglets tabs.forEach(tab => { tab.classList.toggle('active', tab.getAttribute('data-tab') === tabType); }); // Supprimer le contenu existant sauf les onglets const groupList = this.shadowRoot?.querySelector('#group-list'); if (!groupList) return; const children = Array.from(groupList.children); children.forEach(child => { if (!child.classList.contains('tabs')) { groupList.removeChild(child); } }); // Charger le contenu approprié switch (tabType) { case 'processes': const processSet = await this.getProcessesWhereTheCurrentMemberIs(); await this.loadAllProcesses(processSet); break; case 'members': await this.lookForMyDms(): await this.loadAllMembers(); break; default: console.error('Unknown tab type:', tabType); } } //load all processes from the service private async loadAllProcesses(processSet: Set) { console.log('🎯 Loading all processes'); this.closeSignature(); const allProcesses = await this.getProcesses(); // Afficher les processus dans le container #group-list const groupList = this.shadowRoot?.querySelector('#group-list'); if (!groupList) { console.warn('⚠️ Group list element not found'); return; } groupList.innerHTML = ''; const tabContent = document.createElement('div'); tabContent.className = 'tabs'; tabContent.innerHTML = ` `; groupList.appendChild(tabContent); // Ajouter les event listeners const tabs = tabContent.querySelectorAll('.tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { const tabType = tab.getAttribute('data-tab'); if (tabType) { this.switchTab(tabType, tabs); } }); }); //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; }); for (const process of allProcesses) { 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); 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); } li.setAttribute('data-process-id', oneProcess); //----MANAGE THE CLICK ON PROCESS ---- li.onclick = async (event) => { event.stopPropagation(); console.log("CLICKED ON PROCESS:", oneProcess); //viser le h1 de signature-header const signatureHeader = this.shadowRoot?.querySelector('.signature-header h1'); if (signatureHeader) { const emoji = await addressToEmoji(oneProcess); signatureHeader.textContent = `Signature of ${emoji}`; } this.openSignature(); //afficher les roles dans chaque processus 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); }); li.appendChild(container); // afficher les roles dans chaque processus //console.log('🎯 Roles:', roles); 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(); 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'; }); } } private async newRequest(processId: string) { const emoji = await addressToEmoji(processId); const members = await this.getMembersFromProcess(processId); const newRequestButton = this.shadowRoot?.querySelector('#request-document-button'); if (newRequestButton) { newRequestButton.replaceWith(newRequestButton.cloneNode(true)); const freshButton = this.shadowRoot?.querySelector('#request-document-button'); freshButton?.addEventListener('click', async () => { const membersList = await this.generateMembersList(members); const modal = document.createElement('div'); modal.className = 'request-modal'; const today = new Date().toISOString().split('T')[0]; modal.innerHTML = ` `; this.shadowRoot?.appendChild(modal); this.handleFileUpload(modal); this.handleRequestButton(modal); const closeButton = modal.querySelector('.close-modal'); closeButton?.addEventListener('click', () => { modal.remove(); }); }); } } //request button in the modal private handleRequestButton(modal: HTMLElement) { const requestButton = modal.querySelector('#send-request-button'); requestButton?.addEventListener('click', () => { console.log("REQUEST SENT"); if (modal) { //vérifier qu'au moins un membre est coché const membersList = modal.querySelector('.members-list-modal'); if (membersList) { const members = membersList.querySelectorAll('.member-checkbox:checked'); if (members.length === 0) { alert('Please select at least one member'); return; } } //vérifier que la date est valide const dateInput = modal.querySelector('#date-input') as HTMLInputElement; if (dateInput) { const date = new Date(dateInput.value); if (isNaN(date.getTime())) { alert('Please select a valid date'); return; } } //verifier qu'un fichier a été load const fileList = modal.querySelector('#file-list'); if (fileList && fileList.children.length === 0) { alert('Please upload at least one file'); return; } //récupérer le message const messageInput = modal.querySelector('#message-input') as HTMLTextAreaElement; if (messageInput) { const message = messageInput.value; } //modal.remove(); } }); } private handleFileUpload(modal: HTMLElement) { const fileInput = modal.querySelector('#file-input') as HTMLInputElement; const fileList = modal.querySelector('#file-list'); const selectedFiles = new Set(); fileInput?.addEventListener('change', () => { if (fileList && fileInput.files) { Array.from(fileInput.files).forEach(file => { if (!Array.from(selectedFiles).some(f => f.name === file.name)) { selectedFiles.add(file); const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = ` ${file.name} `; fileList.appendChild(fileItem); fileItem.querySelector('.remove-file')?.addEventListener('click', () => { selectedFiles.delete(file); fileItem.remove(); }); } }); fileInput.value = ''; } }); return selectedFiles; } private async generateMembersList(members: string[]) { let html = ''; for (const member of members) { const emoji = await addressToEmoji(member); html += `
  • ${emoji}
  • `; } return html; } //Send a set of members from a process private async getMembersFromProcess(processId: string) { const service = await Services.getInstance(); const process = await service.getProcess(processId); console.log("Process récupéré:", process); // Récupérer les rôles directement depuis le dernier état const roles = await service.getRoles(process); console.log("Roles trouvés:", roles); if (!roles) return []; type RoleData = { members?: { sp_addresses?: string[] }[]; }; const uniqueMembers = new Set(); Object.values(roles as unknown as Record).forEach((roleData: RoleData) => { roleData.members?.forEach((member) => { if (member.sp_addresses && member.sp_addresses[0]) { uniqueMembers.add(member.sp_addresses[0]); } }); }); return Array.from(uniqueMembers); } private async loadAllRolesAndMembersInSignature(roles: any) { console.log('🎯 Roles:', roles); const signatureDescription = this.shadowRoot?.querySelector('.signature-description'); if (signatureDescription) { signatureDescription.innerHTML = ''; Object.entries(roles).forEach(([roleName, roleData]: [string, any]) => { const roleItem = document.createElement('li'); roleItem.className = 'role-signature'; const roleContainer = document.createElement('div'); roleContainer.className = 'role-signature-container'; const roleNameSpan = document.createElement('span'); roleNameSpan.className = 'role-signature-name'; roleNameSpan.textContent = roleName; const uniqueMembers = new Map(); roleData.members?.forEach((member: any) => { const spAddress = member.sp_addresses?.[0]; if (spAddress && !uniqueMembers.has(spAddress)) { uniqueMembers.set(spAddress, member); } }); 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); signatureDescription.appendChild(roleItem); }); } } //fonction qui ferme la signature private closeSignature() { const closeSignature = this.shadowRoot?.querySelector('#close-signature'); const signatureArea = this.shadowRoot?.querySelector('.signature-area'); if (closeSignature && signatureArea) { closeSignature.addEventListener('click', () => { signatureArea.classList.add('hidden'); }); } } //fonction qui ouvre la signature private openSignature() { const signatureArea = this.shadowRoot?.querySelector('.signature-area'); if (signatureArea) { signatureArea.classList.remove('hidden'); } } //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(); } //fonction qui renvoie les processus où le sp_adress est impliqué private async getProcessesWhereTheCurrentMemberIs() { const service = await Services.getInstance(); try { const currentMember = await service.getMemberFromDevice(); if (!currentMember) { console.error('❌ Pas de membre trouvé'); return this.userProcessSet; } const pairingProcess = await this.getMyProcessId(); const memberEmoji = await addressToEmoji(pairingProcess); console.log("Mon adresse:", currentMember[0], memberEmoji); const processes = await service.getProcesses(); for (const [processId, process] of Object.entries(processes)) { try { const roles = process.states[0]?.roles; if (!roles) { console.log(`Pas de rôles trouvés pour le processus ${processId}`); continue; } for (const roleName in roles) { const role = roles[roleName]; if (role.members && Array.isArray(role.members)) { for (const member of role.members) { if (member.sp_addresses && Array.isArray(member.sp_addresses)) { if (member.sp_addresses.includes(currentMember[0])) { this.userProcessSet.add(processId); console.log(`Ajout du process ${processId} au Set (trouvé dans le rôle ${roleName})`); break; } } } } } } catch (e) { console.log(`Erreur lors du traitement du processus ${processId}:`, e); continue; } } return this.userProcessSet; } catch (e) { console.error('❌ Erreur:', e); return this.userProcessSet; } } // Load the group list from all processes public async loadAllGroupListFromMyProcess(): Promise { 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 { 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(); 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; if (file.size > MAX_FILE_SIZE) { alert('Le fichier est trop volumineux. Taille maximum : 1MB'); return; } try { const service = await Services.getInstance(); const myAddresses = await service.getMemberFromDevice(); if (!myAddresses) throw new Error('No paired member found'); let fileData: string; if (file.type.startsWith('image/')) { fileData = await this.compressImage(file); } else { fileData = await this.readFileAsBase64(file); } const timestamp = Date.now(); const processId = this.getAttribute('process-id'); const uniqueKey = `${processId}${timestamp}`; const dbRequest = indexedDB.open('4nk'); dbRequest.onerror = (event) => { console.error("Database error:", dbRequest.error); }; dbRequest.onsuccess = async (event) => { const db = dbRequest.result; const transaction = db.transaction(['diffs'], 'readwrite'); const store = transaction.objectStore('diffs'); try { // Message du fichier const fileTemplate = { value_commitment: uniqueKey, messaging_id: processId, description: 'message_content', metadata: { text: `Fichier envoyé: ${file.name}`, timestamp: timestamp, sender: myAddresses[0], recipient: this.selectedMember, messageState: this.messageState, roleName: this.selectedRole, type: 'file', fileName: file.name, fileType: file.type, fileData: fileData } }; await new Promise((resolve, reject) => { const request = store.add(fileTemplate); request.onsuccess = () => { console.log('✅ File message saved'); resolve(); }; request.onerror = () => reject(request.error); }); // Réponse automatique const autoReplyTemplate = { value_commitment: `${processId}${timestamp + 1000}`, messaging_id: processId, description: 'message_content', metadata: { text: "J'ai bien reçu votre fichier 📎", timestamp: timestamp + 1000, sender: this.selectedMember, recipient: myAddresses[0], messageState: this.messageState, roleName: this.selectedRole } }; await new Promise((resolve, reject) => { const request = store.add(autoReplyTemplate); request.onsuccess = () => { console.log('✅ Auto reply saved'); if (myAddresses[0]) { this.addNotification(myAddresses[0], autoReplyTemplate); } resolve(); }; request.onerror = () => reject(request.error); }); // Attendre la fin de la transaction await new Promise((resolve, reject) => { transaction.oncomplete = () => { console.log('✅ Transaction completed'); resolve(); }; transaction.onerror = () => reject(transaction.error); }); // Réinitialiser l'input file const fileInput = this.shadowRoot?.querySelector('#file-input') as HTMLInputElement; if (fileInput) fileInput.value = ''; // Recharger les messages if (this.selectedMember) { await this.loadMemberChat(this.selectedMember); } } catch (error) { console.error('❌ Transaction error:', error); } }; } catch (error) { console.error('❌ Error in sendFile:', error); } } private async readFileAsBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); }); } private async compressImage(file: File): Promise { return new Promise((resolve, reject) => { const img = new Image(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); img.onload = () => { let width = img.width; let height = img.height; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; ctx?.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL('image/jpeg', 0.7)); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } private async getProcesses(): Promise { const service = await Services.getInstance(); const processes = await service.getProcesses(); const res = Object.entries(processes).map(([key, value]) => ({ key, value, })); return res; } async connectedCallback() { this.processId = this.getAttribute('process-id'); if (this.processId) { console.log("🔍 Chargement du chat avec processID"); await this.loadGroupListFromAProcess(this.processId); } else { 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...'); } window.addEventListener('process-updated', async (e: CustomEvent) => { const processId = e.detail.processId; if (processId === this.processId) { setTimeout(async () => await this.reloadMemberChat(this.selectedMember), 3000); } }); } } customElements.define('chat-element', ChatElement); export { ChatElement };