diff --git a/public/style/chat.css b/public/style/chat.css index 1b54b14..e0f8228 100755 --- a/public/style/chat.css +++ b/public/style/chat.css @@ -226,15 +226,9 @@ body { } .message-container { - max-width: 100%; - border-radius: 5px; - overflow-wrap: break-word; - word-wrap: break-word; - background-color: #f1f1f1; display: flex; - flex-direction: column; + margin: 8px; } - .message-container .message { align-self: flex-start; } @@ -246,27 +240,27 @@ body { } .message { - padding: 12px 18px; - background-color: #e1e1e1; - border-radius: 15px; max-width: 70%; - font-size: 16px; - line-height: 1.4; - margin-bottom: 0%; - white-space: pre-wrap; - word-wrap: break-word; - position: relative; - display: inline-block; + padding: 10px; + border-radius: 12px; + background:var(--secondary-color); + margin: 2px 0; } /* Messages de l'utilisateur */ .message.user { - background-color: #3498db; + background: #2196f3; color: white; - align-self: flex-end; - text-align: right; } +.message-time { + font-size: 0.7em; + opacity: 0.7; + margin-left: 0px; + margin-top: 5px; +} + + /* Amélioration de l'esthétique des messages */ /* .message.user:before { content: ''; diff --git a/src/pages/chat/chat.ts b/src/pages/chat/chat.ts index 75047a8..63f6e7c 100755 --- a/src/pages/chat/chat.ts +++ b/src/pages/chat/chat.ts @@ -9,18 +9,19 @@ declare global { import { groupsMock } from '../../mocks/mock-signature/groupsMock'; import { messagesMock as initialMessagesMock, messagesMock } from '../../mocks/mock-signature/messagesMock'; import { membersMock } from '../../mocks/mock-signature/membersMocks'; +import { ApiReturn, Device, Member } from '../../../pkg/sdk_client'; import { Message, DocumentSignature, } from '../../models/signature.models'; import { messageStore } from '../../utils/messageMock'; -import { Member } from '../../interface/memberInterface'; import { Group } from '../../interface/groupInterface'; 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'; - -let currentUser: Member = membersMock[0]; +const storageUrl = `/storage`; interface LocalNotification { memberId: string; @@ -28,6 +29,7 @@ interface LocalNotification { time: string; } + export function initChat() { const chatElement = document.createElement('chat-element'); const container = document.querySelector('.container'); @@ -37,6 +39,12 @@ export function initChat() { } class ChatElement extends HTMLElement { + static get observedAttributes() { + return ['process-id']; + } + + private sdkClient: any; + private processId: string | null = null; private selectedMemberId: string | null = null; private messagesMock: any[] = []; private dom: Node; @@ -57,6 +65,13 @@ class ChatElement extends HTMLElement { this.attachShadow({ mode: 'open' }); this.messagesMock = messageStore.getMessages(); this.dom = getCorrectDOM('signature-element'); + this.processId = this.getAttribute('process-id'); + + // Initialiser sdkClient + this.initSDKClient(); + + // Récupérer le processId depuis l'attribut du composant + console.log('🔍 Constructor - Process ID from element:', this.processId); this.shadowRoot!.innerHTML = ` @@ -95,9 +110,17 @@ class ChatElement extends HTMLElement { `; window.toggleUserList = this.toggleUserList.bind(this); - window.switchUser = this.switchUser.bind(this); window.loadMemberChat = this.loadMemberChat.bind(this); + + 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' && @@ -107,30 +130,38 @@ class ChatElement extends HTMLElement { } }); this.initMessageEvents(); - this.initFileUpload(); + + document.addEventListener('newMessagingProcess', ((event: CustomEvent) => { + console.log('🎯 Received newMessagingProcess event:', event.detail); + this.addNewMessagingProcess(event.detail.processId, event.detail.processName); + }) as EventListener); + } + + 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.loadGroupList(newValue); + } } private initMessageEvents() { - // Pour le bouton Send const sendButton = this.shadowRoot?.querySelector('#send-button'); if (sendButton) { sendButton.addEventListener('click', () => this.sendMessage()); } - // Pour la touche Entrée const messageInput = this.shadowRoot?.querySelector('#message-input'); if (messageInput) { messageInput.addEventListener('keypress', (event: Event) => { - const keyEvent = event as KeyboardEvent; // Cast en KeyboardEvent + const keyEvent = event as KeyboardEvent; if (keyEvent.key === 'Enter' && !keyEvent.shiftKey) { event.preventDefault(); this.sendMessage(); } }); } - } - private initFileUpload() { const fileInput = this.shadowRoot?.querySelector('#file-input') as HTMLInputElement; if (fileInput) { fileInput.addEventListener('change', (event: Event) => { @@ -183,139 +214,448 @@ class ChatElement extends HTMLElement { // Add notification - private addNotification(memberId: string, message: Message) { - // Creating a new notification - const notification = { - memberId, - text: `New message from Member ${memberId}: ${message.text}`, - time: message.time + 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 groupItem = this.shadowRoot?.querySelector('[data-process-id]'); + const processId = groupItem?.getAttribute('data-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 = member?.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.text}`; + } + + // Créer la notification + const notification = { + memberId, + text: notificationText, + time: message.time + }; + + // Ajouter la notification et mettre à jour l'interface + this.notifications.push(notification); + this.renderNotifications(); + this.updateNotificationBadge(); + + } catch (error) { + console.error('Error creating notification:', error); + } + } + + public isPaired(): boolean { + try { + return this.sdkClient.is_paired(); + } catch (e) { + throw new Error(`isPaired ~ Error: ${e}`); + } + } + + public async createMessagingProcess(otherMembers: Member[], relayAddress: string, feeRate: number): Promise { + if (!this.isPaired()) { + throw new Error('Device not paired'); + } + const me = await this.getMemberFromDevice(); + console.log('My SP addresses:', me); + if (!me) { + throw new Error('No paired member in device'); + } + const allMembers: Member[] = otherMembers; + allMembers.push({ sp_addresses: me }); + const meAndOne = [{ sp_addresses: me }, otherMembers.pop()!]; + const everyOneElse = otherMembers; + const messagingTemplate = { + process_id: crypto.randomUUID(), + parent_id: null, + description: 'messaging', + messages: { + state: 'initial', + object: { + type: 'message_list', + content: [], + content_type: { + allowed: ['text', 'file'], + default: 'text' + }, + metadata: { + created_at: Date.now(), + last_updated: Date.now() + } + } + }, + roles: { + public: { + members: allMembers, + validation_rules: [ + { + quorum: 0.0, + fields: ['description', 'roles', 'messages'], + min_sig_member: 0.0, + }, + ], + storages: [storageUrl] + }, + owner: { + members: meAndOne, + validation_rules: [ + { + quorum: 1.0, + fields: ['description', 'roles', 'messages'], + min_sig_member: 1.0, + }, + ], + storages: [storageUrl] + }, + users: { + members: everyOneElse, + validation_rules: [ + { + quorum: 0.0, + fields: ['description', 'roles', 'messages'], + min_sig_member: 0.0, + }, + ], + storages: [storageUrl] + }, + }, }; - // Added notification to list and interface - this.notifications.push(notification); - this.renderNotifications(); - this.updateNotificationBadge(); + try { + return this.sdkClient.create_new_process(JSON.stringify(messagingTemplate), null, relayAddress, feeRate); + } catch (e) { + throw new Error(`Creating process failed: ${e}`); + } } + async getMemberFromDevice(): Promise { + try { + const device = await this.getDeviceFromDatabase(); + if (device) { + const parsed: Device = JSON.parse(device); + const pairedMember = parsed['paired_member']; + return pairedMember.sp_addresses; + } else { + return null; + } + } catch (e) { + throw new Error(`Failed to retrieve paired_member from device: ${e}`); + } + } + + async getDeviceFromDatabase(): Promise { + const db = await Database.getInstance(); + const walletStore = 'wallet'; + try { + const dbRes = await db.getObject(walletStore, '1'); + if (dbRes) { + const wallet = dbRes['device']; + return wallet; + } else { + return null; + } + } catch (e) { + throw new Error(`Failed to retrieve device from db: ${e}`); + } + } + // Send a messsage - private sendMessage() { - const messageInput = this.shadowRoot?.querySelector('#message-input') as HTMLInputElement; - if (!messageInput) return; - const messageText = messageInput.value.trim(); + private async sendMessage() { + const messageInput = this.shadowRoot?.querySelector('#message-input') as HTMLInputElement; + if (!messageInput || !this.selectedMemberId) return; + + const messageText = messageInput.value.trim(); + if (messageText === '') return; - if (messageText === '' || this.selectedMemberId === null) { - return; - } + try { + const myAddresses = await this.getMemberFromDevice(); + if (!myAddresses) { + throw new Error('No paired member found'); + } - const newMessage: Message = { - id: Date.now(), - sender: "4NK", - text: messageText, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - type: 'text' as const - }; - // Add and display the message immediately - messageStore.addMessage(this.selectedMemberId, newMessage); - this.messagesMock = messageStore.getMessages(); - this.loadMemberChat(this.selectedMemberId); - - // Reset the input - messageInput.value = ''; - - // Automatic response after 2 seconds - setTimeout(() => { - if (this.selectedMemberId) { - const autoReply = this.generateAutoReply(`Member ${this.selectedMemberId}`); - messageStore.addMessage(this.selectedMemberId, autoReply); - this.messagesMock = messageStore.getMessages(); - this.loadMemberChat(this.selectedMemberId); - this.addNotification(this.selectedMemberId, autoReply); - } - }, 2000); - } - - // Scroll down the conversation after loading messages - private scrollToBottom(container: Element) { - (container as HTMLElement).scrollTop = (container as HTMLElement).scrollHeight; - } - - - // Load the list of members - private loadMemberChat(memberId: string | number) { - this.selectedMemberId = String(memberId); - const memberMessages = this.messagesMock.find(m => String(m.memberId) === String(memberId)); - - // Find the process and the role of the member - let memberInfo = { processName: '', roleName: '', memberName: '' }; - groupsMock.forEach(process => { - process.roles.forEach(role => { - const member = role.members.find(m => String(m.id) === String(memberId)); - if (member) { - memberInfo = { - processName: process.name, - roleName: role.name, - memberName: member.name - }; - } - }); + const now = new Date(); + const formattedTime = now.toLocaleString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' }); - - const chatHeader = this.shadowRoot?.querySelector('#chat-header'); - const messagesContainer = this.shadowRoot?.querySelector('#messages'); - - if (!chatHeader || !messagesContainer) return; - chatHeader.textContent = `Chat with ${memberInfo.roleName} ${memberInfo.memberName} from ${memberInfo.processName}`; - messagesContainer.innerHTML = ''; - - if (memberMessages) { - memberMessages.messages.forEach((message: Message) => { - const messageElement = document.createElement('div'); - messageElement.className = 'message-container'; - - const messageContent = document.createElement('div'); - messageContent.className = 'message'; - if (message.type === 'file') { - messageContent.innerHTML = `${message.fileName}`; - messageContent.classList.add('user'); - } else { - messageContent.innerHTML = `${message.sender}: ${message.text} ${message.time}`; - if (message.sender === "4NK") { - messageContent.classList.add('user'); + const newMessage = { + id: Date.now(), + sender: myAddresses[0], + text: messageText, + time: formattedTime, + type: 'text', + class: 'message user' + }; + + if (this.selectedMemberId) { + messageStore.addMessage(this.selectedMemberId!, newMessage); + this.messagesMock = messageStore.getMessages(); + } + + // Récupérer le process_id du parent (conversation) + const groupItem = this.shadowRoot?.querySelector('[data-process-id]'); + const parentProcessId = groupItem?.getAttribute('data-process-id'); + + if (!parentProcessId) { + throw new Error('Parent process ID not found'); + } + + const messageTemplate = { + process_id: parentProcessId, + parent_id: null, + description: 'message', + messages: { + state: 'initial', + object: { + type: 'text', + content: messageText, + metadata: { + created_at: formattedTime, + last_updated: formattedTime, + sender: myAddresses[0], + recipient: this.selectedMemberId } } - - messageElement.appendChild(messageContent); - messagesContainer.appendChild(messageElement); - }); - } - - - this.scrollToBottom(messagesContainer); - } + }, + roles: { + public: { + members: [ + { sp_addresses: myAddresses }, + { sp_addresses: [this.selectedMemberId] } + ], + validation_rules: [ + { + quorum: 0.0, + fields: ['description', 'messages'], + min_sig_member: 0.0, + }, + ], + storages: [storageUrl] + }, + owner: { + members: [ + { sp_addresses: myAddresses }, + { sp_addresses: [this.selectedMemberId] } + ], + validation_rules: [ + { + quorum: 1.0, + fields: ['description', 'messages'], + min_sig_member: 1.0, + }, + ], + storages: [storageUrl] + } + } + }; - private toggleMembers(role: { members: { id: string | number; name: string; }[] }, roleElement: HTMLElement) { - let memberList = roleElement.querySelector('.member-list'); - if (memberList) { - (memberList as HTMLElement).style.display = (memberList as HTMLElement).style.display === 'none' ? 'block' : 'none'; + console.log('Message template:', { + timestamp: formattedTime, + template: messageTemplate + }); + + const result = await this.createMessagingProcess( + [{ sp_addresses: [this.selectedMemberId] }], + 'relay_address', + 1 + ); + + + console.log('Final message process:', { + template: messageTemplate, + result: result, + timestamp: new Date().toISOString() + }); + + + messageInput.value = ''; + + this.loadMemberChat(this.selectedMemberId); + + + setTimeout(() => { + const autoReply = this.generateAutoReply(this.selectedMemberId!); + messageStore.addMessage(this.selectedMemberId!, autoReply); + this.messagesMock = messageStore.getMessages(); + this.loadMemberChat(this.selectedMemberId!); + + this.addNotification(this.selectedMemberId!, autoReply); + }, 1000); + + } catch (error) { + console.error('Error sending message:', error); + } + } + + private scrollToBottom(container: Element) { + (container as HTMLElement).scrollTop = (container as HTMLElement).scrollHeight; + } + + + // Load the list of members + private async loadMemberChat(memberId: string | number) { + const myAddresses = await this.getMemberFromDevice(); + if (!myAddresses) { + console.error('No paired member found'); return; } - + + this.selectedMemberId = String(memberId); + const memberMessages = this.messagesMock.find(m => String(m.memberId) === String(memberId)); + + const chatHeader = this.shadowRoot?.querySelector('#chat-header'); + const messagesContainer = this.shadowRoot?.querySelector('#messages'); + + if (!chatHeader || !messagesContainer) return; + + const memberAddress = String(memberId); + const emojis = await addressToEmoji(memberAddress); + chatHeader.textContent = `Chat with ${emojis}`; + messagesContainer.innerHTML = ''; + + if (memberMessages) { + for (const message of memberMessages.messages) { + const messageElement = document.createElement('div'); + messageElement.className = 'message-container'; + + // Ajouter le style pour aligner les messages + if (message.sender === myAddresses[0]) { + messageElement.style.justifyContent = 'flex-end'; + } else { + messageElement.style.justifyContent = 'flex-start'; + } + + const messageContent = document.createElement('div'); + messageContent.className = message.class || 'message'; + + if (message.type === 'file') { + messageContent.innerHTML = ` +
+ ${await addressToEmoji(message.sender)}: + + 📎 ${message.fileName} + +
+
${message.time}
+ `; + + // Ajouter le gestionnaire de clic pour le téléchargement + const fileSpan = messageContent.querySelector('.file-message'); + fileSpan?.addEventListener('click', () => { + const fileKey = `file_${message.id}`; + const fileData = localStorage.getItem(fileKey); + if (fileData) { + // Créer un lien de téléchargement + const link = document.createElement('a'); + link.href = fileData; + link.download = message.fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }); + } else { + messageContent.innerHTML = ` +
+ ${await addressToEmoji(message.sender)}: ${message.text} +
+
${message.time}
+ `; + } + + messageElement.appendChild(messageContent); + messagesContainer.appendChild(messageElement); + } + } + + this.scrollToBottom(messagesContainer); + } + + private async toggleMembers(roleData: any, roleElement: HTMLElement) { + let memberList = roleElement.querySelector('.member-list'); + if (memberList) { + (memberList as HTMLElement).style.display = + (memberList as HTMLElement).style.display === 'none' ? 'block' : 'none'; + return; + } + memberList = document.createElement('ul'); memberList.className = 'member-list'; - - role.members.forEach(member => { - const memberItem = document.createElement('li'); - memberItem.textContent = member.name; - - memberItem.onclick = (event) => { - event.stopPropagation(); - this.loadMemberChat(member.id.toString()); - }; - - memberList.appendChild(memberItem); - }); - + + 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 { + // S'assurer que le SDK est initialisé + if (!this.sdkClient) { + await this.initSDKClient(); + } + + const groupItem = roleElement.closest('[data-process-id]'); + const processId = groupItem?.getAttribute('data-process-id'); + + if (!processId) { + throw new Error('Process ID not found'); + } + + console.log('Creating messaging process with:', { + processId, + member, + sdkClientInitialized: !!this.sdkClient + }); + + const result = await this.createMessagingProcess( + [member], + 'relay_address', + 1 + ); + + console.log('Messaging process created:', { + processId, + template: result, + member: member + }); + + this.loadMemberChat(member.sp_addresses[0]); + } catch (error) { + console.error('Error creating messaging process:', error); + } + }; + + memberList.appendChild(memberItem); + } + } + roleElement.appendChild(memberList); } @@ -385,53 +725,103 @@ class ChatElement extends HTMLElement { } - private loadGroupList(): void { + private async loadGroupList(processId: string): Promise { + console.log('🔍 Loading group list with processId:', processId); const groupList = this.shadowRoot?.querySelector('#group-list'); if (!groupList) return; - groupsMock.forEach(group => { - const li = document.createElement('li'); - li.className = 'group-list-item'; - - // Create a flex container for the name and the icon - const container = document.createElement('div'); - container.className = 'group-item-container'; - - // Span for the process name - const nameSpan = document.createElement('span'); - nameSpan.textContent = group.name; - nameSpan.className = 'process-name'; - - // Add click event to show roles - nameSpan.addEventListener('click', (event) => { - event.stopPropagation(); - this.toggleRoles(group, li); - }); - - // Assemble the elements - container.appendChild(nameSpan); - li.appendChild(container); + groupList.innerHTML = ''; - // Create and append the role list container - const roleList = document.createElement('ul'); - roleList.className = 'role-list'; - roleList.style.display = 'none'; - - // Add roles for this process - group.roles.forEach(role => { - const roleItem = document.createElement('li'); - roleItem.className = 'role-item'; - roleItem.textContent = role.name; - roleItem.onclick = (event) => { - event.stopPropagation(); - this.toggleMembers(role, roleItem); - }; - roleList.appendChild(roleItem); - }); - - li.appendChild(roleList); - groupList.appendChild(li); + const dbRequest = window.indexedDB.open('4nk'); + const db = await new Promise((resolve, reject) => { + dbRequest.onsuccess = () => resolve(dbRequest.result); + dbRequest.onerror = () => reject(dbRequest.error); }); + + const transaction = db.transaction(['processes'], 'readonly'); + const processStore = transaction.objectStore('processes'); + const processRequest = processStore.get(processId); + + const process = await new Promise((resolve, reject) => { + processRequest.onsuccess = () => { + console.log('🔍 Process found:', processRequest.result); + resolve(processRequest.result); + }; + processRequest.onerror = () => reject(processRequest.error); + }); + + if (!process?.states?.[0]?.encrypted_pcd?.roles) { + console.error('❌ Process structure invalid:', process); + return; + } + + const roles = process.states[0].encrypted_pcd.roles; + console.log('🔑 Roles found:', roles); + + 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'; + + // Traiter chaque rôle + Object.entries(roles).forEach(([roleName, roleData]: [string, any]) => { + const roleItem = document.createElement('li'); + roleItem.className = 'role-item'; + + const roleContainer = document.createElement('div'); + roleContainer.className = 'role-item-container'; + + const roleNameSpan = document.createElement('span'); + roleNameSpan.className = 'role-name'; + roleNameSpan.textContent = roleName; + + // Filtrer les membres dupliqués ici, avant de les passer à toggleMembers + const uniqueMembers = new Map(); + roleData.members?.forEach((member: any) => { + const spAddress = member.sp_addresses?.[0]; + if (spAddress && !uniqueMembers.has(spAddress)) { + uniqueMembers.set(spAddress, member); + } + }); + + // Créer un nouveau roleData avec les membres uniques + const filteredRoleData = { + ...roleData, + members: Array.from(uniqueMembers.values()) + }; + + roleContainer.addEventListener('click', (event) => { + event.stopPropagation(); + this.toggleMembers(filteredRoleData, roleItem); + }); + + roleContainer.appendChild(roleNameSpan); + roleItem.appendChild(roleContainer); + roleList.appendChild(roleItem); + }); + + li.appendChild(roleList); + groupList.appendChild(li); } @@ -454,109 +844,306 @@ class ChatElement extends HTMLElement { (userList as HTMLElement).classList.toggle('show'); } - private switchUser(userId: string | number) { - const user = membersMock.find(member => member.id === userId); - if (!user) return; - currentUser = user; - this.updateCurrentUserDisplay(); - const userList = getCorrectDOM('userList') as HTMLElement; - userList?.classList.remove('show'); - } - - // Function to update the display of the current user - private updateCurrentUserDisplay() { - const userDisplay = getCorrectDOM('current-user') as HTMLElement; - if (userDisplay) { - userDisplay.innerHTML = ` - - `; - } - } // Generate an automatic response private generateAutoReply(senderName: string): Message { + const now = new Date(); + const formattedTime = now.toLocaleString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + return { id: Date.now(), sender: senderName, text: "OK...", - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + time: formattedTime, type: 'text' as const }; } // Send a file - private sendFile(file: File) { - console.log('SendFile called with file:', file); - const reader = new FileReader(); - reader.onloadend = () => { - const fileData = reader.result; - const fileName = file.name; - console.log('File loaded:', fileName); + 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 myAddresses = await this.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 newMessage = { + id: Date.now(), + sender: myAddresses[0], + text: `Fichier envoyé: ${file.name}`, + fileName: file.name, + time: new Date().toLocaleString('fr-FR'), + type: 'file', + class: 'message user' + }; + + try { + const fileKey = `file_${newMessage.id}`; + localStorage.setItem(fileKey, fileData); + } catch (storageError) { + console.error('Erreur de stockage du fichier:', storageError); + alert('Erreur lors du stockage du fichier. Essayez avec un fichier plus petit.'); + return; + } if (this.selectedMemberId) { - messageStore.addMessage(this.selectedMemberId, { - id: Date.now(), - sender: "4NK", - fileName: fileName, - fileData: fileData, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - type: 'file' - }); - console.log('Message added to store'); - + messageStore.addMessage(this.selectedMemberId, newMessage); this.messagesMock = messageStore.getMessages(); - this.loadMemberChat(this.selectedMemberId); } - }; - reader.readAsDataURL(file); + + const groupItem = this.shadowRoot?.querySelector('[data-process-id]'); + const parentProcessId = groupItem?.getAttribute('data-process-id'); + + if (!parentProcessId) { + throw new Error('Parent process ID not found'); + } + + const messageTemplate = { + process_id: parentProcessId, + parent_id: null, + description: 'file_message', + messages: { + state: 'initial', + object: { + type: 'file', + content: fileData, + metadata: { + created_at: newMessage.time, + last_updated: newMessage.time, + sender: myAddresses[0], + recipient: this.selectedMemberId, + fileName: file.name, + fileType: file.type + } + } + }, + roles: { + public: { + members: [ + { sp_addresses: myAddresses }, + { sp_addresses: [this.selectedMemberId] } + ], + validation_rules: [ + { + quorum: 0.0, + fields: ['description', 'messages'], + min_sig_member: 0.0, + }, + ], + storages: [storageUrl] + }, + owner: { + members: [ + { sp_addresses: myAddresses }, + { sp_addresses: [this.selectedMemberId] } + ], + validation_rules: [ + { + quorum: 1.0, + fields: ['description', 'messages'], + min_sig_member: 1.0, + }, + ], + storages: [storageUrl] + } + } + }; + + const result = await this.createMessagingProcess( + [{ sp_addresses: [this.selectedMemberId!] }], + 'relay_address', + 1 + ); + + console.log('Final file message process:', { + template: messageTemplate, + result: result, + timestamp: new Date().toISOString() + }); + + const fileInput = this.shadowRoot?.querySelector('#file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + + this.loadMemberChat(this.selectedMemberId!); + + setTimeout(() => { + const autoReply = this.generateAutoReply(this.selectedMemberId!); + messageStore.addMessage(this.selectedMemberId!, autoReply); + this.messagesMock = messageStore.getMessages(); + this.loadMemberChat(this.selectedMemberId!); + + this.addNotification(this.selectedMemberId!, autoReply); + }, 1000); + + } catch (error) { + console.error('Error sending file:', error); + } } - private initializeEventListeners() { - document.addEventListener('DOMContentLoaded', (): void => { + private async readFileAsBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); }); - - // Gestionnaire d'événements pour le chat - const sendBtn = this.shadowRoot?.querySelector('#send-button'); - if (sendBtn) { - sendBtn.addEventListener('click', this.sendMessage.bind(this)); - } - - const messageInput = this.shadowRoot?.querySelector('#message-input'); - if (messageInput) { - messageInput.addEventListener('keypress', (event: Event) => { - if ((event as KeyboardEvent).key === 'Enter') { - event.preventDefault(); - this.sendMessage(); - } - }); - } - - // Gestionnaire pour l'envoi de fichiers - const fileInput = this.shadowRoot?.querySelector('#file-input'); - if (fileInput) { - fileInput.addEventListener('change', (event: Event) => { - const file = (event.target as HTMLInputElement).files?.[0]; - if (file) { - this.sendFile(file); - } - }); - } } - + private async compressImage(file: File): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + img.onload = () => { + // Calculer les nouvelles dimensions + 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); + + // Compression avec qualité réduite + resolve(canvas.toDataURL('image/jpeg', 0.7)); + }; + + img.onerror = reject; + img.src = URL.createObjectURL(file); + }); + } connectedCallback() { - this.updateCurrentUserDisplay(); - this.initializeEventListeners(); - this.loadGroupList(); + if (this.processId) { + console.log('🔍 Loading chat with process ID:', this.processId); + this.loadGroupList(this.processId); + } else { + console.error('❌ No process ID found in element attributes'); + } // Si un membre est sélectionné par défaut, charger ses messages if (this.selectedMemberId) { this.loadMemberChat(this.selectedMemberId); } } + + private addNewMessagingProcess(processId: string, processName: string) { + console.log('🎯 Adding new messaging process:', { processId, processName }); + const groupList = this.shadowRoot?.querySelector('#group-list'); + if (!groupList) { + console.error('Group list not found in shadow DOM'); + return; + } + + // Vérifier si le processus existe déjà + const existingProcess = groupList.querySelector(`[data-process-id="${processId}"]`); + if (existingProcess) { + console.log('Process already exists:', processId); + return; + } + + // Créer le nouveau groupe + const li = document.createElement('li'); + li.className = 'group-list-item'; + li.setAttribute('data-process-id', processId); + + // Créer le conteneur flex + const container = document.createElement('div'); + container.className = 'group-item-container'; + + // Créer un span pour le nom du processus + const nameSpan = document.createElement('span'); + nameSpan.textContent = processName; + nameSpan.className = 'process-name'; + + // Créer un span pour les emojis + const emojiSpan = document.createElement('span'); + emojiSpan.className = 'process-emoji'; + + // Ajouter les emojis de l'adresse + addressToEmoji(processId).then(emojis => { + emojiSpan.textContent = emojis; + }); + + container.appendChild(nameSpan); + container.appendChild(emojiSpan); + li.appendChild(container); + + // Créer la liste des rôles + const roleList = document.createElement('ul'); + roleList.className = 'role-list'; + roleList.style.display = 'none'; + + // Ajouter un rôle par défaut pour le messaging + const roleItem = document.createElement('li'); + roleItem.className = 'role-item'; + roleItem.textContent = 'Messaging'; + roleList.appendChild(roleItem); + + li.appendChild(roleList); + groupList.appendChild(li); + + console.log('🎯 New messaging process added successfully'); + } + + private async initSDKClient() { + try { + // Récupérer l'instance du SDK depuis window ou l'initialiser + this.sdkClient = (window as any).sdk || await this.createSDKClient(); + if (!this.sdkClient) { + throw new Error('Failed to initialize SDK client'); + } + } catch (error) { + console.error('Error initializing SDK client:', error); + } + } + + private async createSDKClient() { + // Implémentez ici la logique de création du SDK client + // Ceci est un exemple, ajustez selon votre implémentation réelle + return new Promise((resolve) => { + // Logique d'initialisation du SDK + resolve({ + is_paired: () => true, // Valeur par défaut pour le test + create_new_process: async (template: string, parentId: string | null, relayAddress: string, feeRate: number) => { + // Implémentation de create_new_process + return { success: true }; + } + }); + }); + } } customElements.define('chat-element', ChatElement); diff --git a/src/pages/process/process.ts b/src/pages/process/process.ts index 9de7994..a051269 100755 --- a/src/pages/process/process.ts +++ b/src/pages/process/process.ts @@ -36,23 +36,39 @@ export async function init() { search_div.appendChild(autocomplete_list); search_div.appendChild(dropdown_icon); - // This is temporary - // Create a new process with hardcoded members for demonstration purpose - await createMessagingProcess(); + const processes = await getProcesses(); - setTimeout(async () => { - const processes = await getProcesses(); - for (const {key, value} of processes) { + for (const {key, value} of processes) { + const processName = await getDescription(key, value); + if (processName) { + console.log('adding process name to list:', processName); + const opt = new Option(processName); + opt.value = processName; + opt.setAttribute('data-process-id', key); + element.add(opt); + } + } + const { autocomplete_options } = getOptions(element); + + if (autocomplete_options.length === 0) { + console.log('No existing processes - creating new messaging process'); + await createMessagingProcess(); + + const updatedProcesses = await getProcesses(); + for (const {key, value} of updatedProcesses) { const processName = await getDescription(key, value); - // const processName = value['description']; if (processName) { - console.log('adding process name to list:', processName); + console.log('adding new process to list:', processName); const opt = new Option(processName); opt.value = processName; + opt.setAttribute('data-process-id', key); element.add(opt); } } - }, 1000) + } else { + console.log('Existing processes found - skipping messaging creation'); + } + // set the wrapper as child (instead of the element) element.parentNode?.replaceChild(wrapper, element); // set element as child of wrapper @@ -60,15 +76,6 @@ export async function init() { wrapper.appendChild(search_div); addPlaceholder(wrapper); - - // const select = document.querySelector(".select-field"); - // for (const process of processeList) { - // const option = document.createElement("option"); - // option.setAttribute("value", process.name); - // option.innerText = process.name; - - // select.appendChild(option); - // } } function removePlaceholder(wrapper: HTMLElement) { @@ -184,12 +191,33 @@ function clearAutocompleteList(select: HTMLSelectElement) { // Populate the autocomplete list following a given query from the user function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) { const { autocomplete_options } = getOptions(select); - console.log('🚀 ~ populateAutocompleteList ~ autocomplete_options:', autocomplete_options); let options_to_show; - if (dropdown) options_to_show = autocomplete_options; - else options_to_show = autocomplete(query, autocomplete_options); + if (dropdown) { + let messagingCounter = 1; + const messagingOptions = select.querySelectorAll('option[value="messaging"]'); + + options_to_show = autocomplete_options.map(option => { + if (option === 'messaging') { + // Récupérer l'élément option correspondant au compteur actuel + const currentOption = messagingOptions[messagingCounter - 1]; + const processId = currentOption?.getAttribute('data-process-id'); + console.log(`Mapping messaging ${messagingCounter} with processId:`, processId); + + const optionText = `messaging ${messagingCounter}`; + messagingCounter++; + + // Stocker le processId dans un attribut data sur le select + select.setAttribute(`data-messaging-id-${messagingCounter - 1}`, processId || ''); + + return optionText; + } + return option; + }); + } else { + options_to_show = autocomplete(query, autocomplete_options); + } const wrapper = select.parentNode; const input_search = wrapper?.querySelector('.search-container'); @@ -225,12 +253,44 @@ function populateAutocompleteList(select: HTMLSelectElement, query: string, drop // Listener to autocomplete results when clicked set the selected property in the select option function selectOption(e: any) { - console.log('🚀 ~ selectOption ~ e:', e); + console.log('🎯 Click event:', e); + console.log('🎯 Target value:', e.target.dataset.value); + const wrapper = e.target.parentNode.parentNode.parentNode; + const select = wrapper.querySelector('select'); const input_search = wrapper.querySelector('.selected-input'); const option = wrapper.querySelector(`select option[value="${e.target.dataset.value}"]`); + + console.log('🎯 Selected option:', option); + console.log('🎯 Process ID:', option?.getAttribute('data-process-id')); - console.log('🚀 ~ selectOption ~ option:', option); + if (e.target.dataset.value.includes('messaging')) { + const messagingNumber = parseInt(e.target.dataset.value.split(' ')[1]); + const processId = select.getAttribute(`data-messaging-id-${messagingNumber}`); + + console.log('🚀 Dispatching newMessagingProcess event:', { + processId, + processName: `Messaging Process ${processId}` + }); + + // Dispatch l'événement avant la navigation + document.dispatchEvent(new CustomEvent('newMessagingProcess', { + detail: { + processId: processId, + processName: `Messaging Process ${processId}` + } + })); + + // Navigation vers le chat + const navigateEvent = new CustomEvent('navigate', { + detail: { + page: 'chat', + processId: processId || '' + } + }); + document.dispatchEvent(navigateEvent); + return; + } option.setAttribute('selected', ''); createToken(wrapper, e.target.dataset.value); if (input_search.value) { diff --git a/src/router.ts b/src/router.ts index 78f37d7..71f8840 100755 --- a/src/router.ts +++ b/src/router.ts @@ -1,5 +1,6 @@ import '../public/style/4nk.css'; import { initHeader } from '../src/components/header/header'; +import { initChat } from '../src/pages/chat/chat'; import Database from './services/database.service'; import Services from './services/service'; import { cleanSubscriptions } from './utils/subscription.utils'; @@ -191,3 +192,18 @@ async function injectHeader() { } (window as any).navigate = navigate; + +document.addEventListener('navigate', ((e: Event) => { + const event = e as CustomEvent<{page: string, processId?: string}>; + if (event.detail.page === 'chat') { + const container = document.querySelector('.container'); + if (container) container.innerHTML = ''; + + initChat(); + + const chatElement = document.querySelector('chat-element'); + if (chatElement) { + chatElement.setAttribute('process-id', event.detail.processId || ''); + } + } +})); diff --git a/src/services/service.ts b/src/services/service.ts index a2203e4..b32d33e 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -11,7 +11,7 @@ import { BackUp } from '~/models/backup.model'; export const U32_MAX = 4294967295; -const storageUrl = `https://demo.4nkweb.com/storage`; +const storageUrl = `/storage`; const BOOTSTRAPURL = [`https://demo.4nkweb.com/ws/`]; const DEFAULTAMOUNT = 1000n; diff --git a/vite.config.ts b/vite.config.ts index 354f182..fe89fd5 100755 --- a/vite.config.ts +++ b/vite.config.ts @@ -55,8 +55,27 @@ export default defineConfig({ }, server: { fs: { - cachedChecks: false + cachedChecks: false, }, port: 3001, + proxy: { + '/storage': { + target: 'https://demo.4nkweb.com', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/storage/, '/storage'), + configure: (proxy, _options) => { + proxy.on('error', (err, _req, _res) => { + console.log('proxy error', err); + }); + proxy.on('proxyReq', (proxyReq, req, _res) => { + console.log('Sending Request:', req.method, req.url); + }); + proxy.on('proxyRes', (proxyRes, req, _res) => { + console.log('Received Response:', proxyRes.statusCode, req.url); + }); + } + } + } }, }); \ No newline at end of file