2025-01-23 11:58:47 +01:00

1316 lines
53 KiB
TypeScript
Executable File

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 = [];
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<string> = new Set();
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot!.innerHTML = `
<style>
${chatStyle}
</style>
<div class="container">
<!-- List of groups -->
<div class="group-list">
<ul id="group-list">
</ul>
</div>
<!-- Chat area -->
<div class="chat-area">
<div class="chat-header" id="chat-header">
<!-- Chat title -->
</div>
<div class="messages" id="messages">
<!-- Messages -->
</div>
<!-- Input area -->
<div class="input-area">
<label for="file-input" class="attachment-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M13.514 2.444l-10.815 10.785c-.449.449-.678 1.074-.625 1.707l.393 4.696c.041.479.422.86.9.9l4.697.394c.633.053 1.258-.177 1.707-.626l11.875-11.844c.196-.196.195-.512 0-.707l-3.536-3.536c-.195-.195-.511-.196-.707 0l-8.878 8.848c-.162.162-.253.382-.253.611v.725c0 .184.148.332.332.332h.725c.229 0 .448-.092.61-.254l7.11-7.08 1.415 1.415-7.386 7.354c-.375.375-.885.586-1.414.586h-2.414c-.555 0-1-.448-1-1v-2.414c0-.53.211-1.039.586-1.414l9.506-9.477c.781-.781 2.049-.781 2.829-.001l4.243 4.243c.391.391.586.902.586 1.414 0 .512-.196 1.025-.587 1.416l-12.35 12.319c-.748.747-1.76 1.164-2.81 1.164-.257 0-6.243-.467-6.499-.487-.664-.052-1.212-.574-1.268-1.267-.019-.242-.486-6.246-.486-6.499 0-1.05.416-2.062 1.164-2.811l10.936-10.936 1.414 1.444z"/>
</svg>
</label>
<input type="file" id="file-input" style="display: none;" />
<textarea id="message-input" rows="3" placeholder="Type your message..."></textarea>
<button id="send-button">Send</button>
</div>
</div>
<!-- Signature -->
<div class="signature-area">
<div class="signature-header">
<h1>Signature</h1>
<button id="close-signature">x</button>
</div>
<div class="signature-content">
<div class="signature-description">
<h2>Description</h2>
</div>
<div class="signature-common">
<h2>Documents in common</h2>
</div>
<div class="signature-by-role">
<h2>Documents by role</h2>
</div>
</div>
</div>
</div>
`;
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());
}
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();
}
});
}
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 = '<div class="no-notification">No notifications available</div>';
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 de l'adresse
const memberEmoji = await addressToEmoji(memberId);
// 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.selectedRole) {
console.error('❌ No role selected');
return;
}
const messageText = messageInput.value.trim();
if (messageText === '') {
console.error('❌ Empty message');
return;
}
try {
const service = await Services.getInstance();
const myAddresses = await service.getMemberFromDevice();
if (!myAddresses) throw new Error('No paired member found');
const timestamp = Date.now();
// Ouvrir IndexedDB
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 {
const message = {
state: this.messageState,
type: 'text',
content: messageText,
metadata: {
createdAt: timestamp,
lastModified: timestamp,
sender: myAddresses,
recipient: this.selectedMember,
}
};
await new Promise<void>((resolve, reject) => {
const request = store.add(message);
request.onsuccess = () => {
console.log('✅ Message saved');
messageInput.value = '';
resolve();
};
request.onerror = () => reject(request.error);
});
// Attendre la fin de la transaction
await new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => {
console.log('✅ Transaction completed');
resolve();
};
transaction.onerror = () => reject(transaction.error);
});
// Recharger les messages
if (this.selectedMember) {
await this.loadMemberChat(this.selectedMember);
}
} catch (error) {
console.error('❌ Transaction error:', error);
}
};
} 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<string | null> {
// 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 this.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() {
console.log('🎯 Loading all members');
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) return;
const service = await Services.getInstance();
const members = await service.getAllMembers();
const memberList = document.createElement('div');
memberList.className = 'member-list active';
for (const [processId, member] of Object.entries(members)) {
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 emojis = await addressToEmoji(processId);
emojiSpan.textContent = `Member : ${emojis}`;
memberContainer.appendChild(emojiSpan);
memberItem.appendChild(memberContainer);
memberList.appendChild(memberItem);
memberItem.addEventListener('click', async () => {
try {
await this.loadMemberChat(member.sp_addresses);
} catch (error) {
console.error('Error loading member chat:', error);
}
});
}
groupList.appendChild(memberList);
}
private async lookForDmProcess(): Promise<string | null> {
// Filter processes for the children of current process
const service = await Services.getInstance();
const processes = await service.getProcesses();
const selectedMember = this.selectedMember;
for (const [processId, process] of Object.entries(processes)) {
const description = await service.getDescription(processId, process);
console.log('Process description:'description);
if (description !== "dm") {
continue;
}
const roles = await this.getRoles(process);
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 processId;
}
return null;
}
private async loadMemberChat(member: string[]) {
if (member.length === 0) {
console.error('Empty member');
return;
}
try {
const service = await Services.getInstance();
const myAddresses = await service.getMemberFromDevice();
if (!myAddresses) {
console.error('No paired member found');
return;
}
// Set the selected member
this.selectedMember = member;
const chatHeader = this.shadowRoot?.querySelector('#chat-header');
const messagesContainer = this.shadowRoot?.querySelector('#messages');
if (!chatHeader || !messagesContainer) return;
const memberId = this.selectedMember[0];
const emojis = await addressToEmoji(memberId);
chatHeader.textContent = `Chat with ${emojis}`;
messagesContainer.innerHTML = '';
let messagesProcess = await this.lookForChildren();
if (messagesProcess === null) {
console.log('Create a new child process');
// We need to create a new process
try {
if (!this.processId) {
console.error('No process id');
return;
}
const res = await service.createDmProcess(member, this.processId);
// 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 child process', processId);
const createPrdReturn = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(createPrdReturn);
const approveChangeReturn = service.approveChange(processId, stateId);
await service.handleApiReturn(approveChangeReturn);
}, 500);
} catch (e) {
console.error(e);
return;
}
while (messagesProcess === null) {
messagesProcess = await this.lookForChildren();
await new Promise(r => setTimeout(r, 500));
}
} else {
console.log('Found child process', messagesProcess);
}
/* 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[] = [];
console.log(messagesProcess);
const childProcess = JSON.parse(messagesProcess);
if (childProcess?.states) {
for (const state of childProcess.states) {
const pcd_commitment = state.pcd_commitment;
if (pcd_commitment) {
const message_hash = pcd_commitment.message;
if (message_hash) {
const diff = await service.getDiffByValue(message_hash);
const message = diff?.new_value;
console.log('message', message);
allMessages.push(message);
}
}
}
}
allMessages.sort((a, b) => a.metadata.createdAt - b.metadata.createdAt);
if (allMessages.length === 0) {
console.log('No messages found');
return;
} else if (allMessages.length > 1 && allMessages[0].metadata.sender === myAddresses[0] && allMessages[0].metadata.recipient === this.selectedMember[0]) {
console.log('Messages found:', allMessages);
for (const message of allMessages) {
const messageElement = document.createElement('div');
messageElement.className = 'message-container';
const isCurrentUser = message.metadata.sender === myAddresses[0];
messageElement.style.justifyContent = isCurrentUser ? 'flex-end' : 'flex-start';
const messageContent = document.createElement('div');
messageContent.className = `message ${isCurrentUser ? 'user' : ''}`;
const senderEmoji = await addressToEmoji(message.metadata.sender);
if (message.type === 'file') {
let fileContent = '';
if (message.content.type.startsWith('image/')) {
fileContent = `
<div class="file-preview">
<img src="${message.content.data}" alt="Image" style="max-width: 200px; max-height: 200px;"/>
</div>
`;
} else {
const blob = this.base64ToBlob(message.content.data, message.content.type);
const url = URL.createObjectURL(blob);
fileContent = `
<div class="file-download">
<a href="${url}" download="${message.content.name}">
📎 ${message.content.name}
</a>
</div>
`;
}
messageContent.innerHTML = `
<div class="message-content">
<strong>${senderEmoji}</strong>: ${fileContent}
</div>
<div class="message-time">
${new Date(message.metadata.createdAt).toLocaleString('fr-FR')}
</div>
`;
} else {
messageContent.innerHTML = `
<div class="message-content">
<strong>${senderEmoji}</strong>: ${message.content}
</div>
<div class="message-time">
${new Date(message.metadata.createdAt).toLocaleString('fr-FR')}
</div>
`;
}
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 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 });
}
private async toggleMembers(roleData: any, roleElement: HTMLElement) {
let memberList = roleElement.querySelector('.member-list');
const roleName = roleElement.querySelector('.role-name')?.textContent || '';
if (memberList) {
(memberList as HTMLElement).style.display =
(memberList as HTMLElement).style.display === 'none' ? 'block' : 'none';
return;
}
memberList = document.createElement('ul');
memberList.className = 'member-list';
if (roleData.members) {
for (const member of roleData.members) {
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';
if (member.sp_addresses?.[0]) {
const emojis = await addressToEmoji(member.sp_addresses[0]);
emojiSpan.textContent = emojis;
}
memberContainer.appendChild(emojiSpan);
memberItem.appendChild(memberContainer);
memberItem.onclick = async (event) => {
event.stopPropagation();
try {
await this.loadMemberChat(member.sp_addresses);
} catch (error) {
console.error('❌ Error handling member click:', error);
}
};
memberList.appendChild(memberItem);
}
}
roleElement.appendChild(memberList);
}
async getRoles(process: Process): Promise<any | null> {
const service = await Services.getInstance();
// Get the `commited_in` value of the last state and remove it from the array
const currentCommitedIn = process.states.pop()?.commited_in;
console.log('Current CommitedIn (roles):' currentCommitedIn);
if (currentCommitedIn === undefined) {
return null; // No states available
}
// Find the last state where `commited_in` is different
let lastDifferentState = process.states.findLast(
state => state.commited_in !== currentCommitedIn
);
if (!lastDifferentState) {
// It means that we only have one state that is not commited yet, that can happen with process we just created
// let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation
lastDifferentState = process.states.pop();
}
if (!lastDifferentState.pcd_commitment) {
return null;
}
console.log('lastDifferentState (roles):'lastDifferentState);
// Take the roles out of the state
const roles = lastDifferentState!.pcd_commitment['roles'];
if (roles) {
const userDiff = await service.getDiffByValue(roles);
if (userDiff) {
console.log("Successfully retrieved userDiff:", userDiff);
return userDiff.new_value;
} else {
console.log("Failed to retrieve a non-null userDiff.");
}
}
return null;
}
private async switchTab(tabType: string, tabs: NodeListOf<Element>) {
// 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':
this.loadAllMembers();
break;
default:
console.error('Unknown tab type:', tabType);
}
}
private async loadAllProcesses(processSet: Set<string>) {
console.log('🎯 Loading all processes');
console.log("Je suis le processSet dans loadAllProcesses :", processSet);
const dbRequest = indexedDB.open('4nk');
return new Promise((resolve, reject) => {
dbRequest.onerror = (event) => {
console.error('❌ Error opening database:', dbRequest.error);
reject(dbRequest.error);
};
dbRequest.onsuccess = async (event) => {
const db = dbRequest.result;
const transaction = db.transaction(['processes'], 'readonly');
const store = transaction.objectStore('processes');
const request = store.getAll();
request.onsuccess = () => {
const processResult = request.result;
console.log('🎯 Processed result:', processResult);
// 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 = `
<button class="tab active" data-tab="processes">Process</button>
<button class="tab" data-tab="members">Members</button>
`;
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
processResult.sort((a, b) => {
const aInSet = this.userProcessSet.has(a.states[0].commited_in);
const bInSet = this.userProcessSet.has(b.states[0].commited_in);
return bInSet ? 1 : aInSet ? -1 : 0;
});
for (const process of processResult) {
const li = document.createElement('li');
li.className = 'group-list-item';
const oneProcess = process.states[0].commited_in;
// 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);
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
const roles = process.states[0].encrypted_pcd.roles;
//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<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) => {
event.stopPropagation();
await this.toggleMembers(filteredRoleData, roleItem);
});
roleContainer.appendChild(roleNameSpan);
roleItem.appendChild(roleContainer);
roleList.appendChild(roleItem);
});
li.appendChild(roleList);
groupList.appendChild(li);
// Ajouter un écouteur d'événements pour gérer le clic sur le container
container.addEventListener('click', (event) => {
event.stopPropagation();
container.classList.toggle('expanded');
const roleList = container.parentElement?.querySelector('.role-list');
const dm = container.parentElement?.querySelector('.dm');
if (roleList) {
// Si le container est expanded, on montre la liste des rôles
if (container.classList.contains('expanded')) {
(roleList as HTMLElement).style.display = 'block';
if (dm) (dm as HTMLElement).style.display = 'block';
} else {
// Sinon on cache la liste des rôles
(roleList as HTMLElement).style.display = 'none';
if (dm) (dm as HTMLElement).style.display = 'none';
}
}
});
}
resolve(processResult);
};
request.onerror = () => {
console.error('❌ Error getting processes:', request.error);
reject(request.error);
};
};
});
}
//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;
}
console.log("Mon adresse:", currentMember[0]);
const processes = await this.getProcesses();
for (const {key, value} of processes) {
const processName = await key;
const roles = await value.states[0].encrypted_pcd.roles;
const hasCurrentUser = Object.values(roles).some(role =>
(role as { members: { sp_addresses: string[] }[] }).members
.some(member => member.sp_addresses.includes(currentMember[0]))
);
if (hasCurrentUser) {
this.userProcessSet.add(processName);
console.log("Ajout du process au Set:", processName);
}
}
return this.userProcessSet;
} catch (e) {
console.error('❌ Erreur:', e);
return this.userProcessSet;
}
}
// 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 this.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', (event) => {
event.stopPropagation();
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');
const roleList = container.parentElement?.querySelector('.role-list');
const dm = container.parentElement?.querySelector('.dm');
if (roleList) {
// Si le container est expanded, on montre la liste des rôles
if (container.classList.contains('expanded')) {
(roleList as HTMLElement).style.display = 'block';
if (dm) (dm as HTMLElement).style.display = 'block';
} else {
// Sinon on cache la liste des rôles
(roleList as HTMLElement).style.display = 'none';
if (dm) (dm as HTMLElement).style.display = '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<void>((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<void>((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<void>((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<string> {
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<string> {
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<any[]> {
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");
await this.getProcessesWhereTheCurrentMemberIs();
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...');
}
}
}
customElements.define('chat-element', ChatElement);
export { ChatElement };