1816 lines
73 KiB
TypeScript
Executable File
1816 lines
73 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 = 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();
|
|
private dmMembersSet: Set<string> = new Set();
|
|
private addressMap: Record<string, string> = {};
|
|
|
|
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" class="close-signature">x</button>
|
|
</div>
|
|
<div class="signature-content">
|
|
<div class="signature-description">
|
|
<h2>Description</h2>
|
|
</div>
|
|
<div class="signature-documents">
|
|
<div class="signature-documents-header">
|
|
<h2>Documents</h2>
|
|
<button id="request-document-button">New request</button>
|
|
</div>
|
|
<div class="signature-documents-list">
|
|
<ul id="signature-documents-list"></ul>
|
|
</div>
|
|
</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();
|
|
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 = '<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 à 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<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 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<string | null> {
|
|
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<string | null> {
|
|
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<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);
|
|
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 = `
|
|
<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 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 = `
|
|
<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 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<string, string> = {};
|
|
|
|
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<string | null> {
|
|
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<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':
|
|
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<string>) {
|
|
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 = `
|
|
<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
|
|
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<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';
|
|
});
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<div class="modal-content">
|
|
<h2>New Request for ${emoji}</h2>
|
|
<button class="close-modal">×</button>
|
|
<div class="modal-members">
|
|
<h2>To:</h2>
|
|
<ul class="members-list-modal">
|
|
${membersList}
|
|
</ul>
|
|
</div>
|
|
<div class="modal-body">
|
|
<textarea id="message-input" placeholder="Write your message here..."></textarea>
|
|
<div class="file-upload-container">
|
|
<input type="file" id="file-input" multiple>
|
|
<div id="file-list"></div>
|
|
</div>
|
|
<span>Select the deadline:</span>
|
|
<input type="date"
|
|
id="date-input"
|
|
min="${today}"
|
|
value="${today}">
|
|
<button id="send-request-button">Send</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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<File>();
|
|
|
|
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 = `
|
|
<span>${file.name}</span>
|
|
<button class="remove-file">×</button>
|
|
`;
|
|
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 += `<li>${emoji}<input type="checkbox" class="member-checkbox" data-member="${member}"></li>`;
|
|
}
|
|
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<string>();
|
|
Object.values(roles as unknown as Record<string, RoleData>).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<string, any>();
|
|
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<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;
|
|
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");
|
|
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 };
|
|
|
|
|