From 29dbbc9ce43338a15fd199d61388de730c473b44 Mon Sep 17 00:00:00 2001 From: Milad Raeisi Date: Tue, 24 Sep 2024 17:31:24 +0400 Subject: [PATCH] Refactor the chat service --- package-lock.json | 10 ++ package.json | 1 + src/app/components/chat/chat.service.ts | 104 ++++++++++-------- .../chat/chats/chats.component.html | 11 +- .../components/chat/chats/chats.component.ts | 2 + src/app/services/indexed-db.service.ts | 11 +- src/app/services/metadata.service.ts | 1 + src/app/shared/ago.pipe.ts | 16 +++ src/app/shared/timestamp.pipe.ts | 16 +++ 9 files changed, 116 insertions(+), 56 deletions(-) create mode 100644 src/app/shared/ago.pipe.ts create mode 100644 src/app/shared/timestamp.pipe.ts diff --git a/package-lock.json b/package-lock.json index 4bcb98c..a2b57c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "localforage": "^1.10.0", "lodash-es": "4.17.21", "luxon": "3.5.0", + "moment": "^2.30.1", "ng-apexcharts": "1.12.0", "ngx-quill": "26.0.8", "nostr-tools": "^2.7.2", @@ -10892,6 +10893,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/package.json b/package.json index 772a574..92b168b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "localforage": "^1.10.0", "lodash-es": "4.17.21", "luxon": "3.5.0", + "moment": "^2.30.1", "ng-apexcharts": "1.12.0", "ngx-quill": "26.0.8", "nostr-tools": "^2.7.2", diff --git a/src/app/components/chat/chat.service.ts b/src/app/components/chat/chat.service.ts index d4f1f4f..e854ef2 100644 --- a/src/app/components/chat/chat.service.ts +++ b/src/app/components/chat/chat.service.ts @@ -60,7 +60,6 @@ export class ChatService implements OnDestroy { async getContact(pubkey: string): Promise { try { if (!pubkey) { - console.error('Public key is undefined.'); return; } @@ -122,6 +121,7 @@ export class ChatService implements OnDestroy { } + async getProfile(): Promise { try { const publicKey = this._signerService.getPublicKey(); @@ -143,23 +143,60 @@ export class ChatService implements OnDestroy { } } + private updateContactInChats(pubKey: string, updatedMetadata: any): void { + const chatToUpdate = this.chatList.find(chat => chat.contact.pubKey === pubKey); + + if (chatToUpdate) { + chatToUpdate.contact = { + ...chatToUpdate.contact, + ...updatedMetadata + }; + + this.chatList = this.chatList.map(chat => chat.contact.pubKey === pubKey ? chatToUpdate : chat); + + this._chats.next(this.chatList); + + this._indexedDBService.saveChat(chatToUpdate); + } + } + + async getChats(): Promise> { const pubkey = this._signerService.getPublicKey(); const useExtension = await this._signerService.isUsingExtension(); const decryptedPrivateKey = await this._signerService.getSecretKey("123"); - this.subscribeToChatList(pubkey, useExtension, decryptedPrivateKey); + const storedChats = await this._indexedDBService.getAllChats(); + if (storedChats && storedChats.length > 0) { + this.chatList = storedChats; + this._chats.next(this.chatList); + } - this.chatList.forEach(chat => this.loadChatHistory(chat.id!)); + setTimeout(async () => { + try { + if (storedChats && storedChats.length > 0) { + const pubkeys = storedChats.map(chat => chat.contact.pubKey); + const metadataList = await this._metadataService.fetchMetadataForMultipleKeys(pubkeys); + metadataList.forEach(metadata => { + this.updateContactInChats(metadata.pubkey, metadata.metadata); + }); + } + } catch (error) { + console.error('Error updating chat contacts metadata:', error); + } + }, 0); + this.subscribeToChatList(pubkey, useExtension, decryptedPrivateKey); return this.getChatListStream(); } subscribeToChatList(pubkey: string, useExtension: boolean, decryptedSenderPrivateKey: string): Observable { - this._relayService.ensureConnectedRelays().then(() => { + this._relayService.ensureConnectedRelays().then(async () => { + const lastSavedTimestamp = await this._indexedDBService.getLastSavedTimestamp(); + const filters: Filter[] = [ - { kinds: [EncryptedDirectMessage], authors: [pubkey], limit: 25 }, - { kinds: [EncryptedDirectMessage], '#p': [pubkey], limit: 25 } + { kinds: [EncryptedDirectMessage], authors: [pubkey], since: Math.floor(lastSavedTimestamp / 1000) }, + { kinds: [EncryptedDirectMessage], '#p': [pubkey], since: Math.floor(lastSavedTimestamp / 1000) } ]; this._relayService.getPool().subscribeMany(this._relayService.getConnectedRelays(), filters, { @@ -174,7 +211,6 @@ export class ChatService implements OnDestroy { if (event.created_at > lastTimestamp) { this.messageQueue.push(event); - await this.processNextMessage(pubkey, useExtension, decryptedSenderPrivateKey); } }, @@ -213,14 +249,14 @@ export class ChatService implements OnDestroy { ); if (decryptedMessage) { - const messageTimestamp = event.created_at * 1000; + const messageTimestamp = event.created_at; this.addOrUpdateChatList(otherPartyPubKey, decryptedMessage, messageTimestamp, isSentByUser); - const chatToUpdate = this.chatList.find(chat => chat.id === otherPartyPubKey); if (chatToUpdate) { + await this._indexedDBService.saveChat(chatToUpdate); + await this._indexedDBService.saveLastSavedTimestamp(messageTimestamp * 1000); this._chat.next(chatToUpdate); - } } } @@ -231,6 +267,7 @@ export class ChatService implements OnDestroy { } } + private addOrUpdateChatList(pubKey: string, message: string, createdAt: number, isMine: boolean): void { const existingChat = this.chatList.find(chat => chat.contact?.pubKey === pubKey); @@ -240,23 +277,21 @@ export class ChatService implements OnDestroy { contactId: pubKey, isMine, value: message, - createdAt: new Date(createdAt).toISOString(), + createdAt: new Date(createdAt * 1000).toISOString(), }; - - const currentChat = this._chat.value; - if (existingChat) { - const messageExists = existingChat.messages?.some(m => m.id === newMessage.id); if (!messageExists) { existingChat.messages = (existingChat.messages || []).concat(newMessage) .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - if (new Date(existingChat.lastMessageAt!).getTime() < createdAt) { + const lastMessageAtTimestamp = Number(existingChat.lastMessageAt) || 0; + + if (lastMessageAtTimestamp < createdAt) { existingChat.lastMessage = message; - existingChat.lastMessageAt = new Date(createdAt).toLocaleDateString() + ' ' + new Date(createdAt).toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit' }); + existingChat.lastMessageAt = createdAt.toString(); } } } else { @@ -272,28 +307,19 @@ export class ChatService implements OnDestroy { displayName: contactInfo.displayName || contactInfo.name || "Unknown" }, lastMessage: message, - lastMessageAt: new Date(createdAt).toLocaleDateString() + ' ' + new Date(createdAt).toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit' }), + lastMessageAt: createdAt.toString(), messages: [newMessage] }; this.chatList.push(newChat); this.fetchMetadataForPubKey(pubKey); } - - this.chatList.sort((a, b) => new Date(b.lastMessageAt!).getTime() - new Date(a.lastMessageAt!).getTime()); - + this.chatList.sort((a, b) => Number(b.lastMessageAt!) - Number(a.lastMessageAt!)); this._chats.next(this.chatList); - - - if (currentChat) { - const restoredChat = this.chatList.find(chat => chat.id === currentChat.id); - if (restoredChat) { - this._chat.next(restoredChat); - } - } } + private fetchMetadataForPubKey(pubKey: string): void { this._metadataService.fetchMetadataWithCache(pubKey) .then(metadata => { @@ -329,16 +355,14 @@ export class ChatService implements OnDestroy { const myPubKey = this._signerService.getPublicKey(); const historyFilter: Filter[] = [ - { kinds: [EncryptedDirectMessage], authors: [myPubKey], '#p': [pubKey] }, - { kinds: [EncryptedDirectMessage], authors: [pubKey], '#p': [myPubKey] } + { kinds: [EncryptedDirectMessage], authors: [myPubKey], '#p': [pubKey], limit: 10 }, + { kinds: [EncryptedDirectMessage], authors: [pubKey], '#p': [myPubKey], limit: 10 } ]; - console.log("Subscribing to history for chat with: ", pubKey); this._relayService.getPool().subscribeMany(this._relayService.getConnectedRelays(), historyFilter, { onevent: async (event: NostrEvent) => { - console.log("Received historical event: ", event); - const isSentByMe = event.pubkey === myPubKey; + const isSentByMe = event.pubkey === myPubKey; const senderOrRecipientPubKey = isSentByMe ? pubKey : event.pubkey; const decryptedMessage = await this.decryptReceivedMessage( event, @@ -348,7 +372,7 @@ export class ChatService implements OnDestroy { ); if (decryptedMessage) { - const messageTimestamp = event.created_at * 1000; + const messageTimestamp = Math.floor(event.created_at / 1000); this.addOrUpdateChatList(pubKey, decryptedMessage, messageTimestamp, isSentByMe); @@ -418,8 +442,6 @@ export class ChatService implements OnDestroy { const cachedChat = chats?.find(chat => chat.id === id); if (cachedChat) { this._chat.next(cachedChat); - console.log("Fetching chat history for: ", this.recipientPublicKey); - this.loadChatHistory(this.recipientPublicKey); return of(cachedChat); } @@ -435,9 +457,6 @@ export class ChatService implements OnDestroy { const updatedChats = chats ? [...chats, newChat] : [newChat]; this._chats.next(updatedChats); this._chat.next(newChat); - - console.log("Fetching chat history for: ", this.recipientPublicKey); - this.loadChatHistory(this.recipientPublicKey); return of(newChat); }) @@ -469,7 +488,6 @@ export class ChatService implements OnDestroy { const cachedChat = chats?.find(chat => chat.id === contact.pubKey); if (cachedChat) { this._chat.next(cachedChat); - console.log("Fetching chat history for: ", contact.pubKey); this.loadChatHistory(contact.pubKey); return of(cachedChat); } @@ -491,8 +509,6 @@ export class ChatService implements OnDestroy { const updatedChats = chats ? [...chats, newChat] : [newChat]; this._chats.next(updatedChats); this._chat.next(newChat); - - console.log("Fetching chat history for: ", contact.pubKey); this.loadChatHistory(contact.pubKey); return of(newChat); @@ -539,7 +555,6 @@ export class ChatService implements OnDestroy { const published = await this._relayService.publishEventToRelays(signedEvent); if (published) { - console.log('Message sent successfully!'); this.message = ''; } else { console.error('Failed to send the message.'); @@ -569,7 +584,6 @@ export class ChatService implements OnDestroy { const published = await this._relayService.publishEventToRelays(signedEvent); if (published) { - console.log('Message sent successfully with extension!'); this.message = ''; } else { console.error('Failed to send the message with extension.'); diff --git a/src/app/components/chat/chats/chats.component.html b/src/app/components/chat/chats/chats.component.html index d2c5d3e..ddb9a2b 100644 --- a/src/app/components/chat/chats/chats.component.html +++ b/src/app/components/chat/chats/chats.component.html @@ -194,7 +194,7 @@
- {{ chat?.contact?.name ? chat.contact.name.charAt(0) : '' }} + {{ chat?.contact?.name ? chat.contact.name.charAt(0) : ''}}
} @@ -220,11 +220,10 @@
-
- {{ chat.lastMessageAt }} -
+
+ {{ chat.lastMessageAt | ago }} +
+ @if (chat.muted) { { try { const chats: Chat[] = []; + await this.chatStore.iterate((value) => { chats.push(value); }); - // مرتب‌سازی چت‌ها بر اساس زمان آخرین پیام به‌طوری که آخرین چت‌ها اول نمایش داده شوند chats.sort((a, b) => { - const dateA = new Date(a.lastMessageAt!).getTime(); - const dateB = new Date(b.lastMessageAt!).getTime(); - return dateB - dateA; // چت‌هایی که تاریخ جدیدتری دارند در ابتدا قرار می‌گیرند + const dateA = Number(a.lastMessageAt); + const dateB = Number(b.lastMessageAt); + return dateB - dateA; }); return chats; @@ -277,7 +277,8 @@ export class IndexedDBService { console.error('Error getting chats from IndexedDB:', error); return []; } -} + } + async saveLastSavedTimestamp(timestamp: number): Promise { diff --git a/src/app/services/metadata.service.ts b/src/app/services/metadata.service.ts index f9e7a75..ba18c23 100644 --- a/src/app/services/metadata.service.ts +++ b/src/app/services/metadata.service.ts @@ -53,6 +53,7 @@ export class MetadataService { const metadata = JSON.parse(event.content); await this.indexedDBService.saveUserMetadata(event.pubkey, metadata); metadataList.push({ pubkey: event.pubkey, metadata }); + } catch (error) { console.error('Error parsing metadata:', error); } diff --git a/src/app/shared/ago.pipe.ts b/src/app/shared/ago.pipe.ts new file mode 100644 index 0000000..005f788 --- /dev/null +++ b/src/app/shared/ago.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as moment from 'moment'; + +@Pipe({ name: 'ago', standalone: true }) +export class AgoPipe implements PipeTransform { + + transform(value: number): string { + + if (value === 0) { + return ''; + } + + const date = moment.unix(value); + return date.fromNow(); + } +} diff --git a/src/app/shared/timestamp.pipe.ts b/src/app/shared/timestamp.pipe.ts new file mode 100644 index 0000000..d2975d9 --- /dev/null +++ b/src/app/shared/timestamp.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as moment from 'moment'; + + +@Pipe({ name: 'timestamp' }) +export class TimestampPipe implements PipeTransform { + transform(value: number): any { + + if (value === 0) { + return ''; + } + + const date = moment.unix(value); + return date.toDate(); + } +}